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.
Prerequisites
- Enable rewarded server-side verification on your ad unit.
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.
RewardedAdsVerifierverifier=newRewardedAdsVerifier.Builder().fetchVerifyingPublicKeysWith(RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD).build();StringrewardUrl=...;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 ServerSideVerificationOptions::custom_data . 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 ServerSideVerificationOptions::user_id . 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 |
AdColony | 15586990674969969776 |
AdColony (bidding) | 6895345910719072481 |
AdFalcon | 3528208921554210682 |
AdMob Network | 5450213213286189855 |
AdMob Network Waterfall | 1215381445328257950 |
Applovin | 1063618907739174004 |
Applovin (bidding) | 1328079684332308356 |
Chartboost | 2873236629771172317 |
Chocolate Platform (bidding) | 6432849193975106527 |
Custom Event | 18351550913290782395 |
DT Exchange* * Prior to September 21, 2022, this network was called "Fyber Marketplace". | 2179455223494392917 |
Equativ (bidding)* * Prior to January 12, 2023, this network was called "Smart Adserver". | 5970199210771591442 |
Fluct (bidding) | 8419777862490735710 |
Flurry | 3376427960656545613 |
Fyber* * This ad source is used for historical reporting. | 4839637394546996422 |
i-mobile | 5208827440166355534 |
Improve Digital (bidding) | 159382223051638006 |
Index Exchange (bidding) | 4100650709078789802 |
InMobi | 7681903010231960328 |
InMobi (bidding) | 6325663098072678541 |
InMobi Exchange (bidding) | 5264320421916134407 |
IronSource | 6925240245545091930 |
ironSource Ads (bidding) | 1643326773739866623 |
Leadbolt | 2899150749497968595 |
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 |
LG U+AD | 18298738678491729107 |
LINE Ads Network | 3025503711505004547 |
Magnite DV+ (bidding) | 3993193775968767067 |
maio | 7505118203095108657 |
maio (bidding) | 1343336733822567166 |
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 |
MoPub (deprecated) | 10872986198578383917 |
myTarget | 8450873672465271579 |
Nend | 9383070032774777750 |
Nexxen (bidding)* * Prior to May 1, 2024, this network was called "UnrulyX". | 2831998725945605450 |
OneTag Exchange (bidding) | 4873891452523427499 |
OpenX (bidding) | 4918705482605678398 |
Pangle | 4069896914521993236 |
Pangle (bidding) | 3525379893916449117 |
PubMatic (bidding) | 3841544486172445473 |
Reservation campaign | 7068401028668408324 |
SK planet | 734341340207269415 |
Sharethrough (bidding) | 5247944089976324188 |
Smaato (bidding) | 3362360112145450544 |
Sonobi (bidding) | 3270984106996027150 |
Tapjoy | 7295217276740746030 |
Tapjoy (bidding) | 4692500501762622178 |
Tencent GDT | 7007906637038700218 |
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.
The following code sample demonstrates how to set custom data on a rewarded ad object before requesting an ad.
firebase::gma::RewardedAd*rewarded_ad;rewarded_ad=newfirebase::gma::RewardedAd();firebase::gma::RewardedAd::ServerSideVerificationOptionsoptions;options.custom_data="SAMPLE_CUSTOM_DATA_STRING";rewarded_ad->SetServerSideVerificationOptions(options);
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.
Stringurl=...;NetHttpTransporthttpTransport=newNetHttpTransport.Builder().build();HttpRequesthttpRequest=httpTransport.createRequestFactory().buildGetRequest(newGenericUrl(url));HttpResponsehttpResponse=httpRequest.execute();if(httpResponse.getStatusCode()!=HttpStatusCodes.STATUS_CODE_OK){thrownewIOException("Unexpected status code = "+httpResponse.getStatusCode());}Stringdata;InputStreamcontentStream=httpResponse.getContent();try{InputStreamReaderreader=newInputStreamReader(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.
privatestaticMap<Integer,ECPublicKey>parsePublicKeysJson(StringpublicKeysJson)throwsGeneralSecurityException{Map<Integer,ECPublicKey>publicKeys=newHashMap<>();try{JSONArraykeys=newJSONObject(publicKeysJson).getJSONArray("keys");for(inti=0;i < keys.length();i++){JSONObjectkey=keys.getJSONObject(i);publicKeys.put(key.getInt("keyId"),EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64"))));}}catch(JSONExceptione){thrownewGeneralSecurityException("failed to extract trusted signing public keys",e);}if(publicKeys.isEmpty()){thrownewGeneralSecurityException("No trusted keys are available.");}returnpublicKeys;}
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 ×tamp=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.
publicstaticfinalStringSIGNATURE_PARAM_NAME="signature=";...URIuri;try{uri=newURI(rewardUrl);}catch(URISyntaxExceptionex){thrownewGeneralSecurityException(ex);}StringqueryString=uri.getQuery();inti=queryString.indexOf(SIGNATURE_PARAM_NAME);if(i==-1){thrownewGeneralSecurityException("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:
publicstaticfinalStringKEY_ID_PARAM_NAME="key_id=";...StringsigAndKeyId=queryString.substring(i);i=sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);if(i==-1){thrownewGeneralSecurityException("needs a key_id query parameter");}Stringsig=sigAndKeyId.substring(SIGNATURE_PARAM_NAME.length(),i-1/* i - 1 instead of i because of & */);intkeyId=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.
privatevoidverify(finalbyte[]dataToVerify,intkeyId,finalbyte[]signature)throwsGeneralSecurityException{Map<Integer,ECPublicKey>publicKeys=parsePublicKeysJson();if(publicKeys.containsKey(keyId)){foundKeyId=true;ECPublicKeypublicKey=publicKeys.get(keyId);EcdsaVerifyJceverifier=newEcdsaVerifyJce(publicKey,HashType.SHA256,EcdsaEncoding.DER);verifier.verify(signature,dataToVerify);}else{thrownewGeneralSecurityException("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.