> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adrop.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 리워드 광고 SSV

> 리워드 광고 보상 완료 시 서버로 전송되는 SSV 콜백 URL을 등록하고 페이로드를 복호화하는 방법을 안내합니다.

## 개요

리워드 광고의 **서버 사이드 검증(SSV, Server-Side Verification)** 을 사용하면, 사용자가 광고 시청을 완료했을 때 Adrop 서버가 지정된 서버 URL로 콜백을 전송합니다. 서버에서 보상 지급을 직접 검증하여 어뷰징을 방지할 수 있어요.

콜백 페이로드는 API 키로 암호화되므로, URL이 외부에 노출되더라도 API 키를 가진 주체만 내용을 복호화할 수 있습니다.

***

## 설정하기

<Steps>
  <Step title="API 키 준비">
    SSV 콜백 페이로드 복호화에 사용할 API 키를 먼저 발급하세요. 프로젝트에 활성 API 키가 **1개 이상** 있어야 SSV를 등록할 수 있어요.

    발급 방법은 [연동 & API 키](/ko/integrations/overview#api-키-발급) 문서를 참고하세요.
  </Step>

  <Step title="SSV 등록">
    콘솔 **\[관리]** > **\[연동]** > **리워드 광고 SSV** 섹션에서 **\[+ 리워드 광고 SSV 등록]** 버튼을 누릅니다.

    | 항목            | 설명                                           |
    | ------------- | -------------------------------------------- |
    | **URL**       | 보상 완료 시 호출할 서버 URL. `https://`로 시작해야 합니다.    |
    | **연결할 API 키** | 콜백 페이로드 복호화에 사용할 API 키. 기본값은 가장 최근에 만든 활성 키. |
  </Step>

  <Step title="SDK에서 userId / customData 전달">
    SDK에서 `setServerSideVerificationOptions()`로 `userId`와 `customData`를 설정하면, 해당 값이 콜백 페이로드에 포함되어 서버로 전달됩니다.

    각 플랫폼별 설정 방법은 아래 문서를 참고하세요.

    * [Android 보상형 광고 — 서버 사이드 검증](/ko/sdk/android/rewarded#서버-사이드-검증)
    * [iOS 보상형 광고 — 서버 사이드 검증](/ko/sdk/ios/rewarded#서버-사이드-검증)
    * [React Native 보상형 광고 — 서버 측 검증](/ko/sdk/react-native/rewarded#서버-측-검증)
    * [Flutter 보상형 광고 — 서버 측 검증](/ko/sdk/flutter/rewarded#서버-측-검증-ssv)
  </Step>
</Steps>

<Note>
  등록/수정 시 HTTPS URL만 허용되며, 사설 IP(localhost, 10.x, 172.16\~31.x, 192.168.x, 169.254.x)는 차단됩니다.
</Note>

***

## 요청 사양

Adrop 서버는 다음과 같이 콜백을 전송합니다.

| 항목           | 값                        |
| ------------ | ------------------------ |
| Method       | `POST`                   |
| Content-Type | `application/json`       |
| 재시도          | 최대 3회 (0ms / 1s / 2s 간격) |
| 타임아웃         | 요청당 5초                   |
| 성공 조건        | HTTP 200 응답              |

요청 바디는 AES-256-GCM으로 암호화되어 전달됩니다.

```json theme={null}
{
  "encrypted": "<ivHex>:<tagHex>:<ciphertextHex>"
}
```

각 구간은 콜론(`:`)으로 구분된 hex 문자열입니다. 복호화 후 얻어지는 평문 JSON 스펙은 다음과 같습니다.

| 필드              | 타입      | 설명                   |
| --------------- | ------- | -------------------- |
| `project`       | string  | Adrop 프로젝트 ID        |
| `app`           | string  | Adrop 앱 ID           |
| `unit`          | string  | Adrop 광고 유닛 ID       |
| `adNetwork`     | string  | 광고 네트워크 식별자          |
| `adUnit`        | string  | 광고 네트워크 내 유닛 식별자     |
| `userId`        | string? | SDK에서 설정한 사용자 식별자    |
| `customData`    | string? | SDK에서 설정한 커스텀 데이터    |
| `rewardItem`    | string  | 보상 아이템명              |
| `rewardAmount`  | number  | 보상 수량                |
| `transactionId` | string  | 트랜잭션 ID (중복 방지용 고유값) |
| `timestamp`     | number  | 콜백 발생 시각 (Unix ms)   |

<Note>
  `transactionId`는 고유값입니다. 서버에서 이미 처리한 `transactionId`를 다시 수신하면 중복 보상을 지급하지 않도록 멱등(idempotent) 처리하는 것을 권장합니다.
</Note>

***

## 페이로드 복호화

AES-256-GCM 키는 **API 키 원문을 SHA-256 해시한 32바이트**입니다.

### Node.js

```javascript theme={null}
import { createDecipheriv, createHash } from 'node:crypto'
import express from 'express'

function decryptSsvPayload(encrypted, apiKey) {
  const [ivHex, tagHex, ciphertextHex] = encrypted.split(':')
  const key = createHash('sha256').update(apiKey).digest() // 32바이트

  const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'))
  decipher.setAuthTag(Buffer.from(tagHex, 'hex'))

  const plaintext = Buffer.concat([
    decipher.update(Buffer.from(ciphertextHex, 'hex')),
    decipher.final()
  ]).toString('utf8')

  return JSON.parse(plaintext)
}

const app = express()

app.post('/ssv-callback', express.json(), (req, res) => {
  const payload = decryptSsvPayload(req.body.encrypted, process.env.ADROP_API_KEY)
  // payload.userId, payload.customData, payload.rewardAmount 등으로 보상 지급 처리
  res.sendStatus(200)
})
```

### Python

```python theme={null}
import hashlib
import json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def decrypt_ssv_payload(encrypted: str, api_key: str) -> dict:
    iv_hex, tag_hex, ciphertext_hex = encrypted.split(':')
    key = hashlib.sha256(api_key.encode()).digest()  # 32바이트

    aesgcm = AESGCM(key)
    # AESGCM은 ciphertext + tag가 이어진 형태를 기대
    plaintext = aesgcm.decrypt(
        bytes.fromhex(iv_hex),
        bytes.fromhex(ciphertext_hex) + bytes.fromhex(tag_hex),
        None
    )
    return json.loads(plaintext)
```

<Warning>
  SSV 콜백은 URL이 외부로 노출되더라도 Adrop API 키를 가진 주체만 복호화할 수 있도록 설계돼 있습니다. **SSV에 연결된 API 키가 노출되면** 서버에서 즉시 폐기하고, 연동 메뉴에서 새 API 키로 교체하세요.
</Warning>

***

## 수정 및 삭제

등록된 SSV는 **리워드 광고 SSV** 섹션 우측 메뉴에서 수정 또는 삭제할 수 있어요.

* **수정**: URL 또는 연결된 API 키를 변경합니다.
* **삭제**: 콜백이 더 이상 호출되지 않습니다. 삭제 후에도 SDK의 `userId` / `customData`는 Adrop 내부 SSV 로그에 저장됩니다.

***

## 관련 문서

<CardGroup cols={2}>
  <Card title="연동 & API 키" icon="key" href="/ko/integrations/overview">
    API 키 발급, 관리, 폐기 방법
  </Card>

  <Card title="리포트 API" icon="chart-line" href="/ko/integrations/reports">
    캠페인 성과 및 백필 수익 데이터를 API로 조회하는 방법
  </Card>
</CardGroup>
