<br />

Server-side verification callbacks are URL requests, with query parameters
expanded by Google, that are sent by Google to an external system to
notify it that a user should be rewarded for interacting with a rewarded or
rewarded interstitial ad. Rewarded SSV (server-side verification) callbacks
provide an extra layer of protection against spoofing of client-side callbacks
to reward users.

This guide shows you how to verify rewarded SSV callbacks by using the
[Tink Java Apps](https://github.com/tink-crypto/tink-java-apps) third-party
cryptographic library to ensure that the query parameters in the callback are
legitimate values.
Although Tink is used for the purposes of this guide, you have the option to
use any third-party library that supports
[ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm).
You can also test your server with the [testing
tool](https://support.google.com/admob/answer/9603226) in the AdMob UI.

> [!IMPORTANT]
> **Key Point:** This guide is not intended to show samples of verifying server-side callback in every language. If you are not comfortable applying these concepts to the ECDSA library of your choice, we recommend using the [client side reward callback](https://developers.google.com/admob/flutter/rewarded.md.txt#show_the_ad) in the Google Mobile Ads SDK to reward users.

## Prerequisites

- Enable [rewarded server-side
  verification](https://support.google.com/admob/answer/7665911) on your ad unit.

## Use RewardedAdsVerifier from the Tink Java Apps library

The [Tink Java Apps](https://github.com/tink-crypto/tink-java-apps) GitHub repository
includes a
[`RewardedAdsVerifier`](https://github.com/tink-crypto/tink-java-apps/blob/main/rewardedads/src/main/java/com/google/crypto/tink/apps/rewardedads/RewardedAdsVerifier.java)
helper class to reduce the code required to verify a rewarded SSV callback.
Using this class enables you to verify a callback URL with the following code.

```java
RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
    .fetchVerifyingPublicKeysWith(
        RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
    .build();
String rewardUrl = ...;
verifier.verify(rewardUrl);
```

If the `verify()` method executes without raising an exception, the callback
URL was successfully verified. The [Rewarding the user](https://developers.google.com/admob/flutter/ssv.md.txt#rewarding_the_user)
section details best practices regarding when users should be rewarded. For a
breakdown of the steps performed by this class to verify rewarded SSV callbacks,
you can read through the [Manual verification of rewarded
SSV](https://developers.google.com/admob/flutter/ssv#manual_verification_of_rewarded_ssv) section.

## SSV callback parameters

Server-side verification callbacks contain query parameters that describe the
rewarded ad interaction. Parameter names, descriptions, and example values are
listed below. Parameters are sent in alphabetical order.

| Parameter Name | Description | Example value |
|---|---|---|
| ad_network | Ad source identifier for the ad source that fulfilled this ad. Ad source names corresponding to ID values are listed in the [Ad source identifiers](https://developers.google.com/admob/flutter/ssv.md.txt#ad-source) section. | 1953547073528090325 |
| ad_unit | AdMob ad unit ID that was used to request the rewarded ad. | 2747237135 |
| custom_data | Custom data string as provided by [`customData`](https://pub.dev/documentation/google_mobile_ads/latest/google_mobile_ads/ServerSideVerificationOptions/customData.html). If no custom data string is provided by the app, this query parameter value will not be present in the SSV callback. | SAMPLE_CUSTOM_DATA_STRING |
| key_id | Key to be used to verify SSV callback. This value maps to a public key provided by the [AdMob key server](https://gstatic.com/admob/reward/verifier-keys.json). | 1234567890 |
| reward_amount | Reward amount as specified in the ad unit settings. | 5 |
| reward_item | Reward item as specified in the ad unit settings. | coins |
| signature | Signature for SSV callback generated by AdMob. | MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY |
| timestamp | Timestamp of when the user was rewarded as Epoch time in ms. | 1507770365237823 |
| transaction_id | Unique hex encoded identifier for each reward grant event generated by AdMob. | 18fa792de1bca816048293fc71035638 |
| user_id | User identifier as provided by [`userId`](https://pub.dev/documentation/google_mobile_ads/latest/google_mobile_ads/ServerSideVerificationOptions/userId.html). If no user identifier is provided by the app, this query parameter will not be present in the SSV callback. | 1234567 |

## Ad source identifiers

### Ad source names and IDs

| Ad source name | Ad source ID |
|---|---|
| Ad Generation (bidding) | `1477265452970951479` |
| Adagio (bidding) | `11706077846011201784` |
| AdMob Network | `5450213213286189855` |
| AdMob Network Waterfall | `1215381445328257950` |
| AppLovin | `1063618907739174004` |
| AppLovin (bidding) | `1328079684332308356` |
| Bidease (bidding) | `3670825090829827805` |
| BidMachine (bidding) | `7943972370566394673` |
| Bigo Ads SDK (bidding) | `504039719704837539` |
| Chartboost | `2873236629771172317` |
| Chocolate Platform (bidding) | `6432849193975106527` |
| Custom Event | `18351550913290782395` |
| DT Exchange\* ~\* Prior to September 21, 2022, this network was called "Fyber Marketplace".~ | `2179455223494392917` |
| DT Exchange (bidding) | `8189833498765234879` |
| Equativ (bidding)\* ~\* Prior to January 12, 2023, this network was called "Smart Adserver".~ | `5970199210771591442` |
| Fluct (bidding) | `8419777862490735710` |
| i-mobile | `5208827440166355534` |
| Improve Digital (bidding) | `159382223051638006` |
| Index Exchange (bidding) | `4100650709078789802` |
| InMobi | `7681903010231960328` |
| InMobi (SDK) (bidding) | `8468954295581492586` |
| InMobi Exchange (bidding) | `5264320421916134407` |
| ironSource Ads | `6925240245545091930` |
| ironSource Ads (bidding) | `1643326773739866623` |
| Liftoff Monetize\* ~\* Prior to January 30, 2023, this network was called "Vungle".~ | `1953547073528090325` |
| Liftoff Monetize (bidding)\* ~\* Prior to January 30, 2023, this network was called "Vungle (bidding)".~ | `4692500501762622185` |
| LY Ads Network | `3025503711505004547` |
| LY Ads Network (bidding) | `2615812619460460513` |
| Magnite DV+ (bidding) | `3993193775968767067` |
| maio | `7505118203095108657` |
| Media.net (bidding) | `2127936450554446159` |
| Mediated house ads | `6060308706800320801` |
| Meta Audience Network\* ~\* Prior to June 6, 2022, this network was called "Facebook Audience Network".~ | `10568273599589928883` |
| Meta Audience Network (bidding)\* ~\* Prior to June 6, 2022, this network was called "Facebook Audience Network (bidding)".~ | `11198165126854996598` |
| Mintegral | `1357746574408896200` |
| Mintegral (bidding) | `6250601289653372374` |
| Mobfox (bidding) | `3086513548163922365` |
| MobileFuse (bidding) | `7303547408604090310` |
| Moloco Ads SDK (bidding) | `8267622065755668722` |
| myTarget | `8450873672465271579` |
| Nativo (bidding) | `3240503836211327896` |
| Nexxen (bidding)\* ~\* Prior to May 1, 2024, this network was called "UnrulyX".~ | `2831998725945605450` |
| OneTag Exchange (bidding) | `4873891452523427499` |
| OpenX (bidding) | `4918705482605678398` |
| Pangle | `4069896914521993236` |
| Pangle KR SDK (bidding) | `12171279046073404914` |
| Pangle ROW SDK (bidding) | `3525379893916449117` |
| Pangle US SDK (bidding) | `15999446638585856012` |
| PubMatic (bidding) | `3841544486172445473` |
| PubMatic OpenWrap SDK | `7702975372504485373` |
| PubMatic OpenWrap SDK (bidding) | `1234567890123456789` |
| Reservation campaign | `7068401028668408324` |
| Rise (bidding) | `6816468518946650043` |
| Sharethrough (bidding) | `5247944089976324188` |
| Smaato (bidding) | `3362360112145450544` |
| Sonobi (bidding) | `3270984106996027150` |
| TripleLift (bidding) | `8332676245392738510` |
| Unity Ads | `4970775877303683148` |
| Unity Ads (bidding) | `7069338991535737586` |
| Verve Group (bidding) | `5013176581647059185` |
| Vpon | `1940957084538325905` |
| Yieldmo (bidding) | `4193081836471107579` |
| YieldOne (bidding) | `3154533971590234104` |
| Zucks | `5506531810221735863` |

## Rewarding the user

It is important to balance user experience and reward validation when deciding
when to reward a user. Server-side callbacks may experience delays before
reaching external systems. Therefore, the recommended best practice is to use
the client-side callback to reward the user immediately, while performing
validation on all rewards upon the receipt of server-side callbacks. This
approach provides a good user experience while ensuring the validity of granted
rewards.

However, for applications where reward validity is critical (for example, the
reward affects your app's in-game economy) and delays in granting rewards are
acceptable, waiting for the verified server-side callback may be the best
approach.

## Custom data

Apps that require extra data in server-side verification callbacks should use
the custom data feature of rewarded ads. Any string value set on a rewarded ad
object is passed to the `custom_data` query parameter of the SSV callback. If no
custom data value is set, the `custom_data` query parameter value won't be
present in the SSV callback.

<br />

The following example sets the SSV options after the rewarded ad is loaded:

```dart
RewardedAd.load(
  adUnitId: "_adUnitId",
  request: AdRequest(),
  rewardedAdLoadCallback: RewardedAdLoadCallback(
    onAdLoaded: (ad) {
ServerSideVerificationOptions _options =
ServerSideVerificationOptions(
customData: 'SAMPLE_CUSTOM_DATA_STRING',
);
      ad.setServerSideOptions(_options);
      _rewardedAd = ad;
    },
    onAdFailedToLoad: (error) {},
  ),
);
```

Replace <var translate="no">SAMPLE_CUSTOM_DATA_STRING</var> with your custom data.

If you want to set the custom reward string, you must do so before showing
the ad.

> [!IMPORTANT]
> **Key Point:** The custom reward string is [percent
> escaped](https://en.wikipedia.org/wiki/Percent-encoding) and might require decoding when parsed from the SSV callback.

## Manual verification of rewarded SSV

The steps performed by the `RewardedAdsVerifier` class to verify a rewarded
SSV are outlined below. Although the included code snippets are in Java and
leverage the Tink third-party library, these steps can be implemented by you in
the language of your choice, using any third-party library that supports
[ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm).

### Fetch public keys

To verify a rewarded SSV callback, you need a public key provided by AdMob.

A list of public keys to be used to validate the rewarded SSV callbacks can be
fetched from the [AdMob key
server](https://gstatic.com/admob/reward/verifier-keys.json). The list of public keys
is provided as a JSON representation with a format similar to the following:

    {
     "keys": [
        {
          keyId: 1916455855,
          pem: "---BEGIN PUBLIC KEY---\nMF...YTPcw==\n---END PUBLIC KEY---"
          base64: "MFkwEwYHKoZIzj0CAQYI...ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
        },
        {
          keyId: 3901585526,
          pem: "---BEGIN PUBLIC KEY---\nMF...aDUsw==\n---END PUBLIC KEY---"
          base64: "MFYwEAYHKoZIzj0CAQYF...4akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
        },
      ],
    }

To retrieve the public keys, connect to the AdMob key server and download the
keys. The following code accomplishes this task and saves the JSON
representation of the keys to the `data` variable.

    String url = ...;
    NetHttpTransport httpTransport = new NetHttpTransport.Builder().build();
    HttpRequest httpRequest =
        httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(url));
    HttpResponse httpResponse = httpRequest.execute();
    if (httpResponse.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
      throw new IOException("Unexpected status code = " + httpResponse.getStatusCode());
    }
    String data;
    InputStream contentStream = httpResponse.getContent();
    try {
      InputStreamReader reader = new InputStreamReader(contentStream, UTF_8);
      data = readerToString(reader);
    } finally {
      contentStream.close();
    }

Note that public keys are regularly rotated. You will receive an email to inform
you of an upcoming rotation. If you're caching public keys, you should update
the keys upon receiving this email.

Once the public keys have been fetched, they must be parsed. The
`parsePublicKeysJson` method below takes a JSON string, such as the example
above, as input, and creates a mapping from `key_id` values to public keys,
which are encapsulated as `ECPublicKey` objects from the Tink library.

    private static Map<Integer, ECPublicKey> parsePublicKeysJson(String publicKeysJson)
        throws GeneralSecurityException {
      Map<Integer, ECPublicKey> publicKeys = new HashMap<>();
      try {
        JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys");
        for (int i = 0; i < keys.length(); i++) {
          JSONObject key = keys.getJSONObject(i);
          publicKeys.put(
              key.getInt("keyId"),
              EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64"))));
        }
      } catch (JSONException e) {
        throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
      }
      if (publicKeys.isEmpty()) {
        throw new GeneralSecurityException("No trusted keys are available.");
      }
      return publicKeys;
    }

### Get content to be verified

The last two query parameters of rewarded SSV callbacks are always `signature`
and `key_id,` in that order. The remaining query parameters specify the content
to be verified. Let's assume you configured AdMob to send reward callbacks to
`https://www.myserver.com/mypath`. The snippet below shows an example rewarded
SSV callback with the content to be verified highlighted.

> [!IMPORTANT]
> **Key Point:** Content from the SSV callback should not be modified in any way (including keeping the order of query parameters).

```
https://www.myserver.com/path?ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins
&timestamp=150777823&transaction_id=12...DEF&user_id=1234567&signature=ME...Z1c&key_id=1268887
```

The code below demonstrates how to parse the content to be verified from a
callback URL as a UTF-8 byte array.

    public static final String SIGNATURE_PARAM_NAME = "signature=";
    ...
    URI uri;
    try {
      uri = new URI(rewardUrl);
    } catch (URISyntaxException ex) {
      throw new GeneralSecurityException(ex);
    }
    String queryString = uri.getQuery();
    int i = queryString.indexOf(SIGNATURE_PARAM_NAME);
    if (i == -1) {
      throw new GeneralSecurityException("needs a signature query parameter");
    }
    byte[] queryParamContentData =
        queryString
            .substring(0, i - 1)
            // i - 1 instead of i because of & in the query string
            .getBytes(Charset.forName("UTF-8"));

### Get signature and key_id from callback URL

Using the `queryString` value from the previous step, parse the `signature` and
`key_id` query parameters from the callback URL as shown below:

    public static final String KEY_ID_PARAM_NAME = "key_id=";
    ...
    String sigAndKeyId = queryString.substring(i);
    i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);
    if (i == -1) {
      throw new GeneralSecurityException("needs a key_id query parameter");
    }
    String sig =
        sigAndKeyId.substring(
            SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */);
    int keyId = Integer.valueOf(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length()));

### Perform verification

The final step is to verify the content of the callback URL with the
appropriate public key. Take the mapping returned from the
`parsePublicKeysJson` method and use the `key_id` parameter from the callback
URL to get the public key from that mapping. Then verify the signature with
that public key. These steps are demonstrated below in the `verify` method.

    private void verify(final byte[] dataToVerify, int keyId, final byte[] signature)
        throws GeneralSecurityException {
      Map<Integer, ECPublicKey> publicKeys = parsePublicKeysJson();
      if (publicKeys.containsKey(keyId)) {
        foundKeyId = true;
        ECPublicKey publicKey = publicKeys.get(keyId);
        EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER);
        verifier.verify(signature, dataToVerify);
      } else {
        throw new GeneralSecurityException("cannot find verifying key with key ID: " + keyId);
      }
    }

If the method executes without throwing an exception, the callback URL was
successfully verified.

## FAQ

Can I cache the public key provided by the AdMob key server?
:   We recommend that you cache the public key provided by the AdMob key
    server to reduce the number of operations required to validate SSV
    callbacks. However, note that public keys are regularly rotated and should not
    be cached for longer than 24 hours.

How frequently are the public keys provided by the AdMob key server rotated?
:   Public keys provided by the AdMob key server are rotated on a variable
    schedule. To ensure that verification of SSV callbacks continues to work as
    intended, public keys should not be cached for longer than 24 hours.

What happens if my server can't be reached?
:   Google expects an `HTTP 200 OK` success status response code for SSV
    callbacks. If your server cannot be reached or does not provide the expected
    response, Google will re-attempt to send SSV callbacks up to five times in
    one-second intervals.

How can I verify that SSV callbacks are coming from Google?
:   Use reverse DNS lookup to verify that SSV callbacks originate from Google.