Watch And Earn docs
v4.0.0
Live demoConsole demo Get help
● Monetise · Ad networks

Ads
one build, three networks.

Use AdMob, Unity Ads and ironSource LevelPlay, alone or together, from one build. Enter the ids in the console and point each network's reward callback to your Cloud Function. Points for a video are credited only by that callback.

AdMobUnity AdsironSource LevelPlay

01How a rewarded video is credited

Server-to-server reward callback flow The app opens an ad session and gets a nonce, the ad SDK shows the video, the ad network server calls the Cloud Function callback, the function checks the signature, then one Firestore transaction removes duplicates, matches the session and writes the points and a ledger row; the app sees the new balance through its live listener. 1 App asks for a session requestAdSession: cap, cooldown returns a one-time nonce adSessions/{nonce}: pending, 15 min 2 Ad SDK shows the video in the app: AdMob, Unity or LevelPlay gets the uid and the nonce 3 Ad network server the user finished the video: the network calls your URL (server to server) 4 Callback endpoint ssvAdmob · ssvUnity ssvIronsource Cloud Functions, HTTP GET 5 Signature check AdMob: Google's public keys others: secret in config/private fails: 403, nothing written 6 Firestore transaction dedupe (ssvEvents), match session points + ledger row (users/{uid}) mismatch: fraudFlags, no credit 7 App sees the balance live listener on users/{uid} waits up to 30 s, then shows “points are being verified” valid Points are written only in step 6. The app never credits a rewarded video itself. No valid callback, no points. Spin and scratch placements grant a spin or card instead of points.
Rewarded video, from the tap to the balance. Steps 4 to 6: firebase/functions/src/ssv/ and adsession.ts.
  1. The app asks for a sessionIt calls requestAdSession, which checks the daily cap and the cooldown, writes adSessions/{nonce} with status pending and returns the one-time nonce. The session expires after 15 minutes (SESSION_TTL_MS in firebase/functions/src/adsession.ts).
  2. The app shows the videoIt hands the user id and the nonce to the ad SDK (app/lib/ads/native_adapters.dart): AdMob as userId + customData, Unity as serverId = uid:nonce, LevelPlay as the init userId + setDynamicUserId(nonce).
  3. The ad network calls your functionAfter the user watched, the network's server calls ssvAdmob, ssvUnity or ssvIronsource. With a waterfall, whichever network served the video calls back.
  4. The function checks the signatureA bad signature answers 403 bad signature; a missing secret (Unity, LevelPlay) answers 403 not configured. Nothing is written in either case.
  5. One Firestore transaction credits onceIt removes duplicates (ssvEvents/{network}_{transaction id}), matches the pending session of the same user, marks it verified, adds the points to users/{uid} and writes a ledger row in users/{uid}/ledger. A mismatch opens a fraud flag instead and credits nothing.
  6. The app shows the new balanceThe app listens to the user document live. After the video it waits up to 30 seconds for the credit; if it has not arrived by then it shows "Thanks for watching! Your points are being verified and will appear in a moment." and the balance updates by itself when the callback lands.
No callback, no points

The app never credits itself. If the callback URL is missing or wrong in the network's dashboard, users watch videos and receive nothing. Set up the callback before you release, and test it (how).

02Choose the networks

The networks are set in console → Mediation: a waterfall the app runs itself (default, AdMob only), or one mediator SDK. See Mediation and placements. All three SDKs are in every build, and the app reads the choice from config/public, so switching needs no new build.

Console → Ads still holds the older single-network settings. The app uses them only when the Mediation and Ad placements values are absent:

Setting (console → Ads)DefaultUsed for
Ad networkAdMobThe fallback for an empty waterfall list, and for the Status checks "Ad unit ids set for the chosen network" and "Rewarded-ad verification secret set".
Interstitial every n-th screen change0Only when ads.placements.interstitial is absent. Use Ad placements instead.
Seconds between interstitials (minimum)120Same.
Banner on the home screenoffOnly when ads.placements.banner is absent.

Users who bought remove-ads, and VIP users while No non-rewarded ads for VIP users is on (default), see no non-rewarded ads (adFree in app/lib/ads/ad_policy.dart). Rewarded videos stay available because the user chooses them.

03Ids and keys per network

Each network has its own section in the console (AdMob units, Unity Ads units, ironSource LevelPlay units). The verification secret is the last field of that section. The Status page warns when the network chosen under Ads lacks its required ids or its verification secret. Check the other networks of your waterfall yourself.

NetworkRequired idsAlso availableVerification secret (console field)
AdMobAndroid rewarded unit, iOS rewarded unitInterstitial, banner, app-open, native and rewarded-interstitial units per platformNone. Verified with Google's public keys.
Unity AdsAndroid and iOS game ids, Android and iOS rewarded units (placement ids)Interstitial and banner unitsUnity S2S secret: sent to you by Unity Ads support (how to ask)
ironSource LevelPlayAndroid and iOS app keys, Android and iOS rewarded unitsInterstitial and banner units (native ads come from AdMob)ironSource private key: the private key shown in the LevelPlay server-to-server callback settings (where)

Secrets are write-only in the console and only an owner can change them. The console stores them encrypted with ADMIN_SECRET_KEY and writes the plain values to config/private, which only Cloud Functions and the console's service account can read (security rules deny every app user). Read Encryption keys before you ever change that key.

04Server-to-server callback URLs

Replace REGION-PROJECT with your region and project id, for example us-central1-my-project. The region is FUNCTIONS_REGION in firebase/functions/.env (default us-central1). firebase deploy prints the function URLs, and Firebase console → Functions lists them. Set the console's Backend → Cloud Functions base URL to https://REGION-PROJECT.cloudfunctions.net as well.

NetworkCallback URLSetup
AdMobhttps://REGION-PROJECT.cloudfunctions.net/ssvAdmobAdMob
Unity Adshttps://REGION-PROJECT.cloudfunctions.net/ssvUnityUnity Ads
ironSource LevelPlayhttps://REGION-PROJECT.cloudfunctions.net/ssvIronsourceLevelPlay

Each endpoint accepts GET and POST and answers 405 to anything else. One Firebase project, one set of URLs: if you have a staging and a production project, give each its own ad units (or its own callback setting), or callbacks reach the wrong project and open unknown_session fraud flags.

Menu names change

Ad network dashboards are redesigned often. The screenshots below were taken on 2026-10-20 in the author's own accounts, with a demo app named "Watch and Earn Demo" and account identifiers covered. If a menu has moved since, the numbered fields keep the same names, and each section links the network's official guide.

05Set up AdMob server-side verification

  1. Open your rewarded ad unitAdMob → Apps → your app → Ad units → your rewarded unit → Advanced settings → Server-side verification (figures 1 and 2). Google's guide: developers.google.com/admob/android/ssv.
  2. Paste the callback URLhttps://REGION-PROJECT.cloudfunctions.net/ssvAdmob. Do it for the Android and the iOS rewarded unit, and for your rewarded-interstitial units if you use that placement: the app sends the same verification data with both formats.
  3. No secretAdMob callbacks are signed by Google. The function checks them against Google's published keys (https://www.gstatic.com/admob/reward/verifier-keys.json, cached for up to 6 hours). There is nothing to enter in the console.
  4. Enter your unit idsConsole → AdMob units. While a unit id is empty the app uses Google's test unit, which is not your ad unit, so your callback URL is not attached to it.
AdMob Ad units list of the app Watch and Earn Demo with the app, Ad units and the rewarded unit highlighted
Figure 1. AdMob → Apps → your app → Ad units.
  1. Your app, under Apps.
  2. Ad units.
  3. Your rewarded ad unit (its id is covered here).
AdMob Edit ad unit page with Advanced settings and the Server-side verification row highlighted
Figure 2. The ad unit's settings.
  1. Advanced settings.
  2. Server-side verification: click the pencil at the end of the row, then Set up and verify callback url.
AdMob Set up and verify callback URL dialog with the callback URL field, Verify url and Use verified url, and Save highlighted
Figure 3. The callback URL dialog. The URL shown is the example us-central1-my-project; use your own region and project, and deploy the functions first so the URL answers.
  1. Callback URL: https://REGION-PROJECT.cloudfunctions.net/ssvAdmob. Leave the user id and custom data fields empty.
  2. Verify url, then Use verified url.
  3. Save.

06Set up Unity Ads S2S callbacks

Unity Ads has no self-service field for this: Unity Ads support registers your callback URL and sends you the secret. Unity's guide: S2S redeem callbacks.

Plan your release around Unity's reply time

The secret comes from a person at Unity Ads support, not from a dashboard, so it is the slowest step of the ad setup. Unity publishes no response time for these requests. Plan for several business days, and allow up to two weeks before you count on Unity rewarded videos. Send the e-mail below as soon as your Unity project and Game IDs exist, well before your release date, and launch with another network first in the rewarded waterfall (see While you wait for the secret). Keep Unity's reply in your password manager: the console never shows the secret again, and you need it if you ever rotate ADMIN_SECRET_KEY.

What to put in the e-mail to Unity Ads support

Fill in the brackets. Your Game IDs are the values you enter in console → Unity Ads units → Android game id and iOS game id; your organisation and project identifiers are shown in your Unity dashboard.

Subject: Enable S2S redeem callbacks for [app name]

Hello Unity Ads support,

please enable server-to-server redeem callbacks for these games and send me the secret (hash key) for verifying them.

Organization ID: [your Unity organization id]
Project ID:      [your Unity project id]
Game ID(s):      Android [android game id], iOS [ios game id]
Platform(s):     [Android / iOS]
Store app id:    Android package [com.example.app], iOS App Store id [1234567890] / bundle id [com.example.app]
Rewarded placement ids: Android [placement id], iOS [placement id]
Callback URL:    https://[REGION]-[PROJECT].cloudfunctions.net/ssvUnity

The app passes the serverId (sid) as "userId:nonce" when it shows a rewarded video.
Our endpoint verifies the hmac parameter and answers HTTP 200 with the body "1".

Contact: [your name], [your e-mail]

Thank you

Where to find the three ids

Unity dashboard, Monetization, Apps, app settings of Watch and Earn Demo with the Game ID row highlighted
Game ID: Unity Cloud → Monetization → Apps → the gear icon of your app. Each platform app (Android, iOS) has its own.
  1. Monetization → Apps, then the gear icon at the end of your app's row.
  2. Game ID: copy it into the e-mail, and into console → Unity Ads units → Android game id (or iOS game id for the iOS app).
Unity dashboard, Development, project settings of Watch and Earn Demo with the Project ID row highlighted
Project ID: Unity Cloud → Development → your project → Settings.
  1. Settings of your project.
  2. Project ID: copy it into the e-mail.
Unity administration, organization settings with the Organization ID row highlighted and account details covered
Organization ID: Unity Cloud → Home → Manage organization → Settings.
  1. Settings under Organization.
  2. Organization ID: copy it into the e-mail.
  1. Enter the secretWhen support replies, paste the secret into console → Unity Ads units → Unity S2S secret (owner only). Paste it without spaces or line breaks.
  2. Check itRun the Unity self-test in Test the callbacks, then watch one real video.

While you wait for the secret

  • There is no unsigned mode. Without the secret, ssvUnity refuses every callback with 403 not configured. Users who watch a Unity rewarded video in that time get nothing, so keep unity out of the rewarded waterfall (console → Mediation) until the secret is set. Unity can still serve interstitials and banners, which need no callback.
  • Earn rewarded videos from another network. Put admob or ironsource first in the rewarded waterfall and set up its callback.
  • Test the app flow on the emulators. Run the backend with npm run serve in firebase/functions and the app with flutter run --dart-define=USE_EMULATOR=true (debug). Debug builds initialise Unity Ads in test mode (testMode: kDebugMode), and when the SDK reports the reward the app completes the session through devCompleteAdSession, which works only in the Functions emulator. This tests the app, the session and the credit, not Unity's signed callback.

The function computes hmac = HMAC-MD5(secret, every other parameter sorted by name, key=value joined with commas), splits sid into uid and nonce, deduplicates on oid and answers 1.

07Set up ironSource LevelPlay S2S callbacks

  1. Open the server-to-server callback settingsLevelPlay → Apps → the … menu of your app → Set S2S callback (figure 1). Unity's LevelPlay guide: Server-to-server callback.
  2. Paste the callback URL and set it Activehttps://REGION-PROJECT.cloudfunctions.net/ssvIronsource (figure 2).
  3. Copy the private keyLevelPlay fills the Private key field for you; copy it, or replace it with a long random string of your own. Then Save.
  4. Enter the same key in the consoleConsole → ironSource LevelPlay units → ironSource private key (owner only).
LevelPlay Apps list with the row menu of Watch and Earn Demo open and Set S2S callback highlighted
Figure 1. LevelPlay → Apps → … → Set S2S callback.
  1. Apps, under LevelPlay.
  2. The … menu at the end of your app's row.
  3. Set S2S callback.
LevelPlay Create S2S reward callback form with Status, Callback URL, Private key and Save highlighted
Figure 2. The S2S reward callback form. The URL shown is the example us-central1-my-project; use your own region and project.
  1. Status: Active.
  2. Callback URL: https://REGION-PROJECT.cloudfunctions.net/ssvIronsource.
  3. Private key: copy it (covered here) and paste it into console → ironSource LevelPlay units → ironSource private key.
  4. Save.

The function checks signature = md5(timestamp + eventId + applicationUserId + rewards + private key) and answers <eventId>:OK. The app initialises LevelPlay with userId = uid (arrives as applicationUserId) and sets setDynamicUserId(nonce) before each rewarded video.

Test one real LevelPlay callback

The parameter name dynamicUserId comes from the ironSource SDK guides, not the S2S settings page; the function also accepts custom_nonce or an applicationUserId of the form uid:nonce. After setup, watch one real rewarded video and check the result (how). If the nonce does not arrive, the callback is recorded as ignored:no_nonce and nothing is credited.

08Enter the secrets in the console

  1. Sign in as the ownerOnly owners can set the verification secrets.
  2. Open the network's sectionUnity Ads units or ironSource LevelPlay units. The secret field is below the unit ids.
  3. Paste and saveThe field never shows the value again, only whether it is set. On save the console writes it to config/private.ssv; Cloud Functions pick it up within about 30 seconds (their config cache).
  4. Check Status"Rewarded-ad verification secret set" covers the network chosen under console → Ads only; check the others with the self-test below.
Console Unity Ads units page with the game and unit id fields and the Unity S2S secret field opened for a new value, showing a placeholder value
Console → Unity Ads units: the Unity S2S secret field below the unit ids. Click Set, paste, then save. The value shown is a placeholder; after saving the field only says whether a secret is set. The LevelPlay section works the same way.

09Test the callbacks

Which operating system these commands need

Every command in this section is written for a bash shell and needs curl and openssl: that is Terminal on macOS or Linux, where both tools are already installed. On Windows they do not run as written in Command Prompt or PowerShell. Use either of these, then paste the commands unchanged:

  • Git Bash - installed with Git for Windows; it brings its own curl and openssl. Open "Git Bash" from the Start menu.
  • WSL (Windows Subsystem for Linux) - run wsl --install in PowerShell as administrator, restart, open the Ubuntu terminal and run sudo apt update && sudo apt install curl openssl (Microsoft's guide).

You can also skip this whole section: watching one real rewarded video (3. A real view below) proves the same thing from inside the app, on any operating system.

1. The endpoint answers

Call the URL without parameters. You should get 403, which proves the function is deployed at that address:

curl -i https://REGION-PROJECT.cloudfunctions.net/ssvUnity
AnswerMeaning
403 bad signatureDeployed and, for Unity and LevelPlay, a secret is set. (AdMob always answers this to an unsigned call.)
403 not configuredDeployed, but config/private has no secret for this network.
404 or a Google error pageWrong region, project or function name, or the functions are not deployed.

2. The secret matches (Unity, LevelPlay)

These commands sign a test callback with the secret you hold, exactly as the function checks it. They run in the bash shell described in the note above. read -rs asks for the secret without showing it or saving it in your shell history. The test callback carries no nonce, so it credits nothing and opens no fraud flag: it is recorded in ssvEvents as ignored:no_nonce.

# Unity Ads: paste the S2S secret, press Enter
read -rs SECRET
OID="selftest-$(date +%s)"
H=$(printf 'oid=%s,sid=selftest' "$OID" | openssl dgst -md5 -hmac "$SECRET" | awk '{print $NF}')
curl -i "https://REGION-PROJECT.cloudfunctions.net/ssvUnity?oid=$OID&sid=selftest&hmac=$H"
# expected: 200 with the body "1"
# ironSource LevelPlay: paste the private key, press Enter
read -rs KEY
TS=$(date -u +%Y%m%d%H%M); EID="selftest$(date +%s)"
SIG=$(printf '%s%s%s%s%s' "$TS" "$EID" selftest 1 "$KEY" | openssl dgst -md5 | awk '{print $NF}')
curl -i "https://REGION-PROJECT.cloudfunctions.net/ssvIronsource?timestamp=$TS&eventId=$EID&applicationUserId=selftest&rewards=1&signature=$SIG"
# expected: 200 with the body "selftest…:OK"

A 403 bad signature here means the secret in the console is not the one you typed. AdMob cannot be self-tested this way because only Google can sign its callbacks.

3. A real view

Watch one rewarded video on a real device with your own unit ids (use the network's test-device setting so you do not click your own live ads). Within seconds the balance goes up, the console Ledger shows an ad row, and ssvEvents has a document with the outcome credited. If not, follow Points not credited after a video.

10Test ids ship by default

You do not have to take that on trust. One command, run on the folder you unzipped, checks every source file for an AdMob id that is not Google's test id, for a network key or game id that ships with a value instead of empty:

node source/tools/check_ad_ids.mjs

It needs only Node (already required to build this item) and runs the same on macOS, Linux and Windows. A clean package prints no author-owned AdMob, Unity Ads or ironSource identifier in this package and exits 0. Run it again after you paste your own ids, and it will list them - that is the point: from then on they are your ids, in your own console, and the ad revenue is yours.

Every network id in this item is typed into the operator console and stored in your own Firebase project. None of them is compiled into the app, so no identifier of the author can collect anything.

The package contains only Google's public AdMob test ids; a test enforces it. When an AdMob unit id is empty, the app falls back to Google's test unit for that format (app/lib/ads/ad_adapter.dart), so you see test ads during development. They never earn anything. The other two networks show nothing until their ids are entered, and a waterfall skips them.

11Replace the AdMob test app ids

Required before release

The Google Mobile Ads plugin needs an AdMob app id inside the app itself. The package ships Google's test app ids:

FileKeyShipped test value
app/android/app/src/main/AndroidManifest.xmlcom.google.android.gms.ads.APPLICATION_IDca-app-pub-3940256099942544~3347511713
app/ios/Runner/Info.plistGADApplicationIdentifierca-app-pub-3940256099942544~1458002511

Replace both with your own app ids from AdMob → Apps → App settings (one Android app, one iOS app).

Tests stay green

app/test/no_cash_out_test.dart checks only the Dart code for AdMob ids, so putting your own app id in AndroidManifest.xml and Info.plist does not break flutter test. Enter your unit ids in the operator console, not in the code.

The Google plugin is in every build and also provides the consent form below, so keep a valid AdMob app id even if you serve ads from another network.

If you use another network on iOS, also add that network's SKAdNetworkItems entries to Info.plist as its integration guide describes. The package lists Google's entry only.

13Check it worked

  • Console → Status shows no warning for "Ad unit ids set for the chosen network" and "Rewarded-ad verification secret set", and no skipped waterfall lines
  • Every network in your rewarded waterfall has its callback URL and secret set, and its self-test answers 200 (Test the callbacks)
  • Watch one video on a real device: the balance goes up within seconds and the console Ledger shows an ad row
  • No new rows in Fraud flags for your own test views