Skip to content

GET /api/v1/admob/ssv

의도

AdMob 리워드 광고 완료 후 Google 서버가 직접 호출하는 SSV(Server-Side Verification) 콜백. ECDSA-SHA256 서명을 검증한 뒤 ad_bonus +1을 원자적으로 지급한다.

클라이언트가 직접 호출하는 엔드포인트가 아님 — Google AdMob 서버만 호출.

인증

  • JWT 불필요 (Google이 직접 호출, 일반 rate limit 제외)
  • ECDSA-SHA256 서명 검증 (signature, key_id 파라미터 사용)
  • 서명 공개키: https://www.gstatic.com/admob/reward/verifier-keys.json (24h TTL 캐시)

입력

Google AdMob이 전달하는 쿼리 파라미터:

파라미터설명
transaction_id광고 시청 고유 ID (멱등성 키)
user_idServerSideVerificationOptions.userId로 앱이 설정한 값 (= dartbrief user ID)
reward_amount보상 수량 (참고용, 현재 항상 1)
reward_item보상 이름 (참고용)
timestampUnix ms
signaturebase64url ECDSA 서명
key_id서명 검증에 쓸 공개키 ID

응답

200 OK — 정상 지급 / 중복 모두 200

Google은 non-200만 재시도하므로, 비즈니스 로직 분기는 모두 200으로 반환.

400 Bad Request

transaction_id, key_id, signature 누락 시.

401 Unauthorized

서명 검증 실패 시. Google은 재시도하지 않으므로 실제 invalid signature에만 발생해야 함.

500 Internal Server Error

DB 오류 시 → Google이 제한 횟수 재시도. transaction_id가 아직 기록되지 않으므로 멱등 재시도 안전.

비즈니스 로직

sql
-- 단일 원자 CTE (멱등성 + 보너스 지급 동시 보장)
WITH record_tx AS (
    INSERT INTO dartbrief.ad_reward_transactions (transaction_id, user_id, created_at)
    VALUES ($1, $2, NOW())
    ON CONFLICT (transaction_id) DO NOTHING
    RETURNING 1
),
init_quota AS ( ... ),
add_bonus AS (
    UPDATE dartbrief.user_analysis_quota
       SET ad_bonus = CASE WHEN quota_month = $3 THEN ad_bonus + 1 ELSE 1 END
     WHERE user_id = $2
       AND EXISTS (SELECT 1 FROM record_tx)
    RETURNING 1
)
  • record_tx INSERT ON CONFLICT DO NOTHING이 gate — 중복 transaction_id면 이후 CTE 전부 skip
  • 월 하드캡 없음 — SSV가 Google 서버에서 실제 광고 시청을 검증하므로 클라이언트 cap 불필요

Flutter 앱 연동

dart
// prod: ServerSideVerificationOptions.userId에 dartbrief user ID 설정
await ad.setServerSideOptions(
  ServerSideVerificationOptions(userId: userId.toString()),
);
// → SSV 콜백 user_id 파라미터로 전달됨
// → 광고 완료 후 클라이언트 별도 claim 불필요

dev flavor는 test ad unit 사용 → SSV 콜백 발생 안 함 → legacy /quota/ad-reward 경로 유지.

Rate limit

일반 IP rate limit 제외 — Google 공유 egress IP에서 발송되므로 버킷 공유 시 429 발생. SSV 엔드포인트는 ECDSA 서명 검증으로 보호.

운영 체크리스트

  • AdMob 콘솔 → 각 광고 단위(dartbrief-aos, dartbrief-ios) → Server-side verification → Callback URL 등록: https://api.dartbrief.com/api/v1/admob/ssv

관련 테이블

  • dartbrief.ad_reward_transactions — transaction_id UNIQUE, user_id FK → users(id) ON DELETE CASCADE
  • dartbrief.user_analysis_quota — ad_bonus, quota_month