تأیید اعتبار سمت سرور (SSV) فراخوانی

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 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. You can also test your server with the testing tool in the AdMob UI.

Check out this fully working example using Java spring-boot.

Prerequisites

Use RewardedAdsVerifier from the Tink Java Apps library

The Tink Java Apps GitHub repository includes a RewardedAdsVerifier 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.

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

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

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

نام منبع آگهی شناسه منبع آگهی
آرکی (مناقصه) 5240798063227064260
تولید آگهی (مناقصه) 1477265452970951479
AdColony 15586990674969969776
AdColony (غیر SDK) (مناقصه) 4600416542059544716
AdColony (مناقصه) 6895345910719072481
AdFalcon 3528208921554210682
شبکه AdMob 5450213213286189855
آبشار شبکه AdMob 1215381445328257950
نتیجه ADR 10593873382626181482
AMoAd 17253994435944008978
آپلووین 1063618907739174004
Applovin (مناقصه) 1328079684332308356
Chartboost 2873236629771172317
پلت فرم شکلات (مناقصه) 6432849193975106527
CrossChannel (MdotM) 9372067028804390441
رویداد سفارشی 18351550913290782395
صرافی DT*
* تا قبل از 21 سپتامبر 2022، این شبکه "Fyber Marketplace" نام داشت.
2179455223494392917
EMX (مناقصه) 8497809869790333482
نوسان (مناقصه) 8419777862490735710
سر و صدا 3376427960656545613
فیبر*
* این منبع آگهی برای گزارش تاریخی استفاده می شود.
4839637394546996422
آی موبایل 5208827440166355534
بهبود دیجیتال (مناقصه) 159382223051638006
بورس شاخص (مناقصه) 4100650709078789802
InMobi 7681903010231960328
InMobi (مناقصه) 6325663098072678541
تبادل InMobi (مناقصه) 5264320421916134407
منبع آهن 6925240245545091930
IronSource Ads (مناقصه) 1643326773739866623
لیدبولت 2899150749497968595
LG U+AD 18298738678491729107
LINE Ads Network 3025503711505004547
مایو 7505118203095108657
maio (مناقصه) 1343336733822567166
Media.net (مناقصه) 2127936450554446159
تبلیغات خانه با واسطه 6060308706800320801
شبکه متا مخاطبان*
* تا قبل از 6 ژوئن 2022، این شبکه «شبکه مخاطبان فیسبوک» نام داشت.
10568273599589928883
شبکه متا مخاطبان (مناقصه)*
* تا قبل از 6 ژوئن 2022، این شبکه "شبکه مخاطبان فیس بوک (مناقصه)" نام داشت.
11198165126854996598
انتگرال 1357746574408896200
منتگرال (مناقصه) 6250601289653372374
موب فاکس 8079529624516381459
MobFox (مناقصه) 3086513548163922365
MoPub ( منسوخ شده ) 10872986198578383917
myTarget 8450873672465271579
نند 9383070032774777750
Nexxen (مناقصه)*

* قبل از 1 می 2024، این شبکه "UnrulyX" نام داشت.

2831998725945605450
ONE توسط AOL (Millennial Media) 6101072188699264581
ONE توسط AOL (Nexage) 3224789793037044399
تبادل OneTag (مناقصه) 4873891452523427499
OpenX (مناقصه) 4918705482605678398
پنگل 4069896914521993236
Pangle (مناقصه) 3525379893916449117
PubMatic (مناقصه) 3841544486172445473
کمپین رزرو 7068401028668408324
RhythmOne (مناقصه) 2831998725945605450
روبیکون (مناقصه) 3993193775968767067
سیاره SK 734341340207269415
اشتراک گذاری (مناقصه) 5247944089976324188
اسماتو (مناقصه) 3362360112145450544
Equativ (مناقصه)*

* قبل از 12 ژانویه 2023، این شبکه "Smart Adserver" نام داشت.

5970199210771591442
سونوبی (مناقصه) 3270984106996027150
Tapjoy 7295217276740746030
Tapjoy (مناقصه) 4692500501762622178
Tencent GDT 7007906637038700218
TripleLift (مناقصه) 8332676245392738510
تبلیغات یونیتی 4970775877303683148
تبلیغات Unity (مناقصه) 7069338991535737586
Verizon Media 7360851262951344112
گروه Verve (مناقصه) 5013176581647059185
Vpon 1940957084538325905
Liftoff کسب درآمد*

* قبل از 30 ژانویه 2023، این شبکه "Vungle" نام داشت.

1953547073528090325
Liftoff کسب درآمد (مناقصه)*

* قبل از 30 ژانویه 2023، این شبکه "Vungle (مناقصه)" نام داشت.

4692500501762622185
بازده (مناقصه) 4193081836471107579
YieldOne (مناقصه) 3154533971590234104
زاکس 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.

The following code sample demonstrates how to set the SSV options after the rewarded ad is loaded.

Java

RewardedAd.load(MainActivity.this, "ca-app-pub-3940256099942544/5354046379",
    new AdRequest.Builder().build(),  new RewardedAdLoadCallback() {
  @Override
  public void onAdLoaded(RewardedAd ad) {
    Log.d(TAG, "Ad was loaded.");
    rewardedAd = ad;
    ServerSideVerificationOptions options = new ServerSideVerificationOptions
        .Builder()
        .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
        .build();
    rewardedAd.setServerSideVerificationOptions(options);
  }
  @Override
  public void onAdFailedToLoad(LoadAdError loadAdError) {
    Log.d(TAG, loadAdError.toString());
    rewardedAd = null;
  }
});

Kotlin

RewardedAd.load(this, "ca-app-pub-3940256099942544/5354046379",
    AdRequest.Builder().build(), object : RewardedAdLoadCallback() {
  override fun onAdLoaded(ad: RewardedAd) {
    Log.d(TAG, "Ad was loaded.")
    rewardedInterstitialAd = ad
    val options = ServerSideVerificationOptions.Builder()
        .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
        .build()
    rewardedAd.setServerSideVerificationOptions(options)
  }

  override fun onAdFailedToLoad(adError: LoadAdError) {
    Log.d(TAG, adError?.toString())
    rewardedAd = null
  }
})

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

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.

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

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.