> ## 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.

# Reward Ad SSV

> Register the SSV callback URL that receives reward-completion events for rewarded ads and decrypt the payload.

## Overview

With **Server-Side Verification (SSV)** for rewarded ads, Adrop sends a callback to your server URL when a user completes watching an ad. This lets you verify reward grants on your own server and prevent abuse.

The callback payload is encrypted with your API key, so even if the URL is exposed, only a party holding the API key can decrypt its contents.

***

## Setup

<Steps>
  <Step title="Prepare an API key">
    Create an API key first — it will be used to decrypt the SSV callback payload. SSV registration requires **at least one** active API key in the project.

    See [Integrations & API Keys](/integrations/overview#generating-an-api-key) for instructions.
  </Step>

  <Step title="Register the SSV">
    Under **\[Management]** > **\[Integrations]** > **Reward Ad SSV**, click **\[+ Register Reward Ad SSV]**.

    | Field       | Description                                                                                     |
    | ----------- | ----------------------------------------------------------------------------------------------- |
    | **URL**     | Server URL to call when a reward is completed. Must start with `https://`.                      |
    | **API key** | API key used to decrypt the callback payload. Defaults to the most recently created active key. |
  </Step>

  <Step title="Send userId / customData from the SDK">
    Set `userId` and `customData` via `setServerSideVerificationOptions()` in the SDK. These values are included in the callback payload.

    See the SDK guides for each platform:

    * [Android Rewarded — Server-Side Verification](/sdk/android/rewarded#server-side-verification)
    * [iOS Rewarded — Server-Side Verification](/sdk/ios/rewarded#server-side-verification)
    * [React Native Rewarded — Server-Side Verification](/sdk/react-native/rewarded#server-side-verification)
    * [Flutter Rewarded — Server-Side Verification](/sdk/flutter/rewarded#server-side-verification-ssv)
  </Step>
</Steps>

<Note>
  Only HTTPS URLs are accepted at registration/edit time. Private IPs (localhost, 10.x, 172.16–31.x, 192.168.x, 169.254.x) are blocked.
</Note>

***

## Request Specification

Adrop delivers the callback as follows.

| Item         | Value                                      |
| ------------ | ------------------------------------------ |
| Method       | `POST`                                     |
| Content-Type | `application/json`                         |
| Retries      | Up to 3 attempts (0ms / 1s / 2s intervals) |
| Timeout      | 5 seconds per attempt                      |
| Success      | HTTP 200 response                          |

The request body is AES-256-GCM encrypted.

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

Each segment is a hex string separated by colons (`:`). After decryption, the plaintext JSON has the following shape:

| Field           | Type    | Description                             |
| --------------- | ------- | --------------------------------------- |
| `project`       | string  | Adrop project ID                        |
| `app`           | string  | Adrop app ID                            |
| `unit`          | string  | Adrop ad unit ID                        |
| `adNetwork`     | string  | Ad network identifier                   |
| `adUnit`        | string  | Unit identifier within the ad network   |
| `userId`        | string? | User identifier set via the SDK         |
| `customData`    | string? | Custom data set via the SDK             |
| `rewardItem`    | string  | Reward item name                        |
| `rewardAmount`  | number  | Reward amount                           |
| `transactionId` | string  | Unique transaction ID for deduplication |
| `timestamp`     | number  | Callback time (Unix ms)                 |

<Note>
  `transactionId` is unique. If your server receives a `transactionId` that was already processed, handle it idempotently to avoid granting duplicate rewards.
</Note>

***

## Decrypting the Payload

The AES-256-GCM key is **the raw API key hashed with SHA-256 (32 bytes)**.

### 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 bytes

  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)
  // Grant rewards based on payload.userId, payload.customData, payload.rewardAmount, etc.
  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 bytes

    aesgcm = AESGCM(key)
    # AESGCM expects ciphertext concatenated with the authentication tag
    plaintext = aesgcm.decrypt(
        bytes.fromhex(iv_hex),
        bytes.fromhex(ciphertext_hex) + bytes.fromhex(tag_hex),
        None
    )
    return json.loads(plaintext)
```

<Warning>
  SSV callbacks are designed so that only a party holding the Adrop API key can decrypt them, even if the URL is exposed. **If the API key connected to the SSV is leaked**, revoke it immediately on the server side and replace it with a new API key via the Integrations menu.
</Warning>

***

## Editing and Deleting

Use the row menu in the **Reward Ad SSV** section to edit or delete an existing SSV.

* **Edit**: Change the URL or the connected API key.
* **Delete**: Callbacks will no longer be invoked. SDK-side `userId` / `customData` values continue to be stored in Adrop's internal SSV log.

***

## Related

<CardGroup cols={2}>
  <Card title="Integrations & API Keys" icon="key" href="/integrations/overview">
    How to generate, manage, and revoke API keys
  </Card>

  <Card title="Report API" icon="chart-line" href="/integrations/reports">
    Query campaign performance and backfill revenue data via API
  </Card>
</CardGroup>
