---
id: "ad-monetization/rewards"
title: "Granting Ad Rewards"
description: "Grant a reward to users when they complete an AdMob rewarded or rewarded interstitial ad. RevenueCat verifies each reward server-side using AdMob Server-Side Verification (SSV) before granting it, so rewards can't be spoofed by a tampered client. You configure what each ad unit grants in the RevenueCat dashboard — no backend of your own required."
permalink: "/docs/ad-monetization/rewards"
slug: "rewards"
version: "current"
original_source: "docs/ad-monetization/rewards.mdx"
---

> **AI agents:** This is the Markdown version of a RevenueCat documentation page. For the complete documentation index, see [llms.txt](https://www.revenuecat.com/docs/llms.txt).

Grant a reward to users when they complete an AdMob **rewarded** or **rewarded interstitial** ad. RevenueCat verifies each reward server-side using AdMob [Server-Side Verification (SSV)](https://support.google.com/admob/answer/9603226) before granting it, so rewards can't be spoofed by a tampered client. You configure what each ad unit grants in the RevenueCat dashboard — no backend of your own required.

Verified rewards can grant [in-app currency](https://www.revenuecat.com/docs/offerings/virtual-currency), a temporary [entitlement](https://www.revenuecat.com/docs/getting-started/entitlements), or both. An ad unit can have one in-app currency reward and multiple entitlement rewards at the same time — completing the ad grants all of them together.

:::warning[Beta Feature]
This feature is currently in beta.
:::

## How it works

1. A user completes a rewarded ad.
2. AdMob sends an SSV callback to RevenueCat, with the user attached by the SDK.
3. RevenueCat verifies the reward and grants a reward based on your configuration.
4. The SDK reports the verified result to your app.

## Prerequisites

- The [AdMob Adapter SDK](https://www.revenuecat.com/docs/getting-started/adapter-sdks/admob) is installed and the RevenueCat SDK is configured.
- SDK minimums: `purchases-ios` 5.80.3+ (iOS 15+) or `purchases-android` 10.12.0+. The reward verification APIs are experimental and require explicit opt-in.
- Your AdMob account is [connected to RevenueCat](https://www.revenuecat.com/docs/integrations/third-party-integrations/google-admob) so your rewarded ad units sync to the dashboard.

## Step 1: Configure ad units in AdMob

For each rewarded ad unit you want to grant a reward for, enable **Server-side verification** in the AdMob console and set the SSV callback URL to:

```
https://api.revenuecat.com/v1/incoming-webhooks/admob-ssv-rewarded
```

This is a single, shared endpoint — use the same URL for every ad unit. RevenueCat resolves the ad unit and the user from data the SDK attaches at show time. See Google's guide on [setting up SSV](https://support.google.com/admob/answer/9603226) for where to enter the URL.

![The Server-side verification field on an AdMob rewarded ad unit, set to the RevenueCat SSV callback URL](https://www.revenuecat.com/docs_images/ad-monetization/admob-ssv-callback-url.png)

## Step 2: Configure the reward in the RevenueCat dashboard

On the **Rewards** page (under **Ads**) in your RevenueCat dashboard, add a rule for each reward you want an ad unit to grant. The page has a section for each reward type; an ad unit can appear in both.

### In-app currency rewards

In the **In-app currencies** section, select **Add currency reward**:

1. Select a synced rewarded ad unit.
2. Choose the in-app currency to grant.
3. Enter the amount (a positive whole number).

![The Add reward dialog in the RevenueCat dashboard, with fields for the ad unit, in-app currency, and amount](https://www.revenuecat.com/docs_images/ad-monetization/add-reward-dialog-2.png)

RevenueCat grants this amount every time a reward from that ad unit is verified. An ad unit can have at most one in-app currency reward.

### Entitlement rewards

In the **Entitlements** section, select **Add entitlement reward**:

1. Select a synced rewarded ad unit.
2. Choose the entitlement to grant. Only active (non-archived) entitlements are selectable — [create one](https://www.revenuecat.com/docs/getting-started/entitlements) first if you don't see it in the list.
3. Enter a duration and pick a unit (minutes, hours, days, weeks, months, or years). The minimum duration is 30 minutes.

![The Add reward dialog in the RevenueCat dashboard, with fields for the ad unit, entitlement, and duration](https://www.revenuecat.com/docs_images/ad-monetization/add-entitlement-reward-dialog.png)

RevenueCat grants the entitlement for the configured duration every time a reward from that ad unit is verified. An ad unit can have multiple entitlement rewards, but only one reward per entitlement — configure another ad unit if you need different durations for the same entitlement.

:::warning[Use a real ad unit]
Entitlement rewards are verified through AdMob SSV, which is configured per ad unit in your own AdMob account. Google's sample test ad unit IDs aren't part of any account, so SSV can't be enabled on them.
:::

## Step 3: Implement it in your app

Enable verification on each ad after it loads, then present it with the verification callbacks.

An ad can grant more than one reward at once. Verification exposes the primary reward and any additional rewards separately, so check both:

- The primary reward — inspect it directly for its type.
- Additional rewards — a list of the same reward type, holding everything beyond the primary reward. Empty when only one reward was granted.

Before delivering the result, the SDK already applies the reward locally: it invalidates the in-app currencies cache for any in-app currency reward, and refreshes customer info for any entitlement reward. You don't need to refresh anything yourself — refetch in-app currency balances only when your UI needs the updated total (see [reading balances](https://www.revenuecat.com/docs/offerings/virtual-currency#reading-balances)), and [check the entitlement status](https://www.revenuecat.com/docs/getting-started/entitlements#checking-entitlement-status) as usual.

```swift
// Opt in to the experimental reward verification APIs.
@_spi(Experimental) import RevenueCatAdMob

RewardedAd.loadAndTrack(
    withAdUnitID: "AD_UNIT_ID",
    request: Request(),
    placement: "bonus_coins",
    fullScreenContentDelegate: self
) { ad, error in
    if error != nil { return }
    guard let ad else { return }

    // Enable RevenueCat reward verification before presenting the ad.
    ad.enableRewardVerification()
    self.rewardedAd = ad
}

// Later, present the ad with verification callbacks:
rewardedAd?.present(
    from: self,
    rewardVerificationStarted: {
        // RevenueCat is verifying the reward server-side.
        // Show a loading state here if you gate the reward on the result.
    },
    rewardVerificationCompleted: { result in
        guard let primaryReward = result.verifiedReward else {
            // Verification did not succeed — for example AdMob rejected the reward,
            // the callback timed out, or a network error occurred. Do not grant the
            // reward client-side as a fallback; let the user retry with another ad.
            return
        }

        // An ad can grant more than one reward; handle the primary reward and any others the same way.
        for reward in [primaryReward] + result.moreRewards {
            self.handleAdReward(reward)
        }
    }
)

func handleAdReward(_ reward: AdReward) {
    if let virtualCurrency = reward.virtualCurrency {
        // RevenueCat already granted the reward server-side and invalidated the local
        // virtual currencies cache; the amount and currency come from the reward rule
        // you configured in the dashboard.
        print("Granted \(virtualCurrency.amount) \(virtualCurrency.code)")

        // Refetch balances only if your UI needs the updated total.
        Purchases.shared.virtualCurrencies { _, _ in
            // Update your UI with the refreshed balances.
        }
    } else if let entitlement = reward.entitlement {
        // RevenueCat already granted the entitlement server-side and refreshed customer
        // info before delivering this result — check entitlement status as usual.
        print("Granted entitlement \(entitlement.identifier) until \(entitlement.expiresAt)")
    }
}
```

```kotlin
@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun loadRewardedAd(context: Context) {
    Purchases.sharedInstance.adTracker.loadAndTrackRewardedAd(
        context = context,
        adUnitId = "AD_UNIT_ID",
        adRequest = AdRequest.Builder().build(),
        placement = "bonus_coins",
        loadCallback = object : RewardedAdLoadCallback() {
            override fun onAdLoaded(ad: RewardedAd) {
                // Enable RevenueCat reward verification before presenting the ad.
                ad.enableRewardVerification()
                rewardedAd = ad
            }

            override fun onAdFailedToLoad(error: LoadAdError) {
                rewardedAd = null
            }
        },
    )
}

// Later, show the ad with verification callbacks:
@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun showRewardedAd(activity: Activity) {
    rewardedAd?.show(
        activity = activity,
        rewardVerificationStarted = {
            // RevenueCat is verifying the reward server-side.
            // Show a loading state here if you gate the reward on the result.
        },
        rewardVerificationCompleted = { result ->
            rewardedAd = null

            val primaryReward = result.verifiedReward
            if (primaryReward != null) {
                // An ad can grant more than one reward; handle the primary reward and any others the same way.
                (listOf(primaryReward) + result.moreRewards).forEach(::handleAdReward)
            } else {
                // Verification did not succeed — for example AdMob rejected the reward,
                // the callback timed out, or a network error occurred. Do not grant the
                // reward client-side as a fallback; let the user retry with another ad.
            }
        },
    )
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun handleAdReward(reward: VerifiedReward) {
    when (reward) {
        is VerifiedReward.VirtualCurrency -> {
            // RevenueCat already granted the reward server-side and invalidated the local
            // virtual currencies cache; the amount and currency come from the reward rule
            // you configured in the dashboard.
            println("Granted ${reward.amount} ${reward.code}")

            // Refetch balances only if your UI needs the updated total.
            Purchases.sharedInstance.getVirtualCurrenciesWith(
                onError = { /* Handle error */ },
                onSuccess = { /* Update your UI with the refreshed balances */ },
            )
        }
        is VerifiedReward.Entitlement -> {
            // RevenueCat already granted the entitlement server-side and refreshed customer
            // info before delivering this result — check entitlement status as usual.
            println("Granted entitlement ${reward.identifier} until ${reward.expiresAt}")
        }
        else -> Unit
    }
}
```

Rewarded interstitial ads work identically — use `RewardedInterstitialAd` in place of `RewardedAd`.

The adapter also automatically tracks the reward-verification funnel — earned, verified, granted, and failed-to-verify — as events tied to the same ad it already tracks loads, impressions, and revenue for. No extra code needed. Requires `purchases-ios` 5.85.0+ or `purchases-android` 10.18.0+.

## Manual integration

The [AdMob Adapter SDK](https://www.revenuecat.com/docs/getting-started/adapter-sdks/admob) used above is only available for iOS and Android — if you use it, Step 3 is all you need. Otherwise — on **Flutter**, **React Native**, **Unity**, or **Kotlin Multiplatform** (none of which have an adapter), or on **iOS/Android when you integrate Google Mobile Ads directly** — do it manually, calling the reward verification methods yourself:

1. After the ad loads, generate a token with `generateRewardVerificationToken` and attach it to the ad's server-side verification options (your ad network's `userId` and `customData` fields).
2. Present the ad.
3. When the ad's earned-reward callback fires, call `pollRewardVerification` with the token's client transaction ID, then handle the result exactly as above (primary reward + additional rewards).
4. Optionally, pass ad metadata to `pollRewardVerification` so the SDK tracks the reward-verification funnel (earned, verified, granted, or failed to verify) the same way the adapter does automatically in Step 3 — omit it to poll without tracking. This uses the same `AdMediatorName`/`AdFormat` types as [manual ad tracking](https://www.revenuecat.com/docs/ad-monetization/manual-integration), and correlates with the ad's other tracked events through a shared `impressionId`.

These methods are ad-library-agnostic: they only produce the `userId`/`customData` you hand to whatever server-side verification mechanism your ad SDK exposes. The examples below use an AdMob library such as [`google_mobile_ads`](https://pub.dev/packages/google_mobile_ads) (Flutter), [`react-native-google-mobile-ads`](https://github.com/invertase/react-native-google-mobile-ads) (React Native), and the [Google Mobile Ads Unity plugin](https://developers.google.com/admob/unity/quick-start) (Unity). Google Mobile Ads has no Kotlin Multiplatform artifact, so on **Kotlin Multiplatform** loading and presenting the ad stays platform-specific — wire it behind an `expect`/`actual` pair backed by `play-services-ads` on Android and the GoogleMobileAds SDK on iOS; only the verification calls shown below are shared.

Additional requirements for this path:

- **Flutter:** `purchases_flutter` 10.10.0+, plus your AdMob library.
- **React Native:** `react-native-purchases` 10.8.0+, plus your AdMob library.
- **Unity:** `purchases-unity` 9.9.0+, plus the Google Mobile Ads Unity plugin.
- **Kotlin Multiplatform:** `purchases-kmp` 3.6.0+, plus your platform-specific Google Mobile Ads setup.
- **iOS / Android:** `purchases-ios` 5.84.0+ or `purchases-android` 10.17.0+; you wire Google Mobile Ads directly instead of installing the adapter.

```swift
// Opt in to the experimental reward verification APIs. No AdMob adapter required.
@_spi(Experimental) import RevenueCat
import GoogleMobileAds

RewardedAd.load(with: "AD_UNIT_ID", request: Request()) { ad, error in
    if error != nil { return }
    guard let ad else { return }

    // Use the loaded ad's response ID as the impression ID, generate a token,
    // and attach it to AdMob's server-side verification options before presenting.
    let impressionId = ad.responseInfo.responseIdentifier ?? ""
    let token = Purchases.shared.generateRewardVerificationToken(impressionId: impressionId)

    let options = ServerSideVerificationOptions()
    options.userIdentifier = token.appUserID
    options.customRewardText = token.customData
    ad.serverSideVerificationOptions = options

    self.rewardedAd = ad
    self.clientTransactionID = token.clientTransactionID
    self.impressionId = impressionId
}

// Later, present the ad. When the user earns the reward, poll RevenueCat for the
// verified result using the token's client transaction ID. Pass trackingMetadata so
// the SDK tracks the reward funnel (earned, verified, granted, failed to verify) the
// same way the AdMob adapter does automatically; omit it to poll without tracking.
rewardedAd?.present(from: self) {
    Task {
        let result = await Purchases.shared.pollRewardVerification(
            clientTransactionID: self.clientTransactionID,
            trackingMetadata: RewardedAdTrackingMetadata(
                networkName: nil,          // e.g., "Google Ads" (optional)
                mediatorName: .adMob,
                adFormat: .rewarded,
                placement: "home_screen",  // Your custom placement ID (optional)
                adUnitId: "AD_UNIT_ID",
                impressionId: self.impressionId
            )
        )

        guard let primaryReward = result.verifiedReward else {
            // Verification did not succeed — for example AdMob rejected the reward,
            // the callback timed out, or a network error occurred. Do not grant the
            // reward client-side as a fallback; let the user retry with another ad.
            return
        }

        // An ad can grant more than one reward; handle the primary reward and any others the same way.
        for reward in [primaryReward] + result.moreRewards {
            self.handleAdReward(reward)
        }
    }
}

func handleAdReward(_ reward: AdReward) {
    if let virtualCurrency = reward.virtualCurrency {
        // RevenueCat already granted the reward server-side and invalidated the local
        // virtual currencies cache; the amount and currency come from the reward rule
        // you configured in the dashboard.
        print("Granted \(virtualCurrency.amount) \(virtualCurrency.code)")
    } else if let entitlement = reward.entitlement {
        // RevenueCat already granted the entitlement server-side and refreshed customer
        // info before delivering this result — check entitlement status as usual.
        print("Granted entitlement \(entitlement.identifier) until \(entitlement.expiresAt)")
    }
}
```

```kotlin
// No AdMob adapter required — call the reward verification methods directly.
@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun loadRewardedAd(context: Context) {
    RewardedAd.load(
        context,
        "AD_UNIT_ID",
        AdRequest.Builder().build(),
        object : RewardedAdLoadCallback() {
            override fun onAdLoaded(ad: RewardedAd) {
                // Use the loaded ad's response ID as the impression ID, generate a token,
                // and attach it to AdMob's server-side verification options before showing.
                val responseId = ad.responseInfo.responseId ?: ""
                val token = Purchases.sharedInstance.generateRewardVerificationToken(responseId)

                ad.setServerSideVerificationOptions(
                    ServerSideVerificationOptions.Builder()
                        .setUserId(token.appUserID)
                        .setCustomData(token.customData)
                        .build(),
                )
                rewardedAd = ad
                clientTransactionId = token.clientTransactionId
                impressionId = responseId
            }

            override fun onAdFailedToLoad(error: LoadAdError) {
                rewardedAd = null
            }
        },
    )
}

// Later, show the ad. When the user earns the reward, poll RevenueCat for the
// verified result using the token's client transaction ID. Pass trackingMetadata so
// the SDK tracks the reward funnel (earned, verified, granted, failed to verify) the
// same way the AdMob adapter does automatically; omit it to poll without tracking.
@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun showRewardedAd(activity: Activity) {
    rewardedAd?.show(activity, OnUserEarnedRewardListener {
        Purchases.sharedInstance.pollRewardVerification(
            clientTransactionId = clientTransactionId,
            callback = object : PollRewardVerificationCallback {
                override fun onCompleted(result: RewardVerificationResult) {
                    rewardedAd = null

                    val primaryReward = result.verifiedReward
                    if (primaryReward != null) {
                        // An ad can grant more than one reward; handle the primary reward and any others the same way.
                        (listOf(primaryReward) + result.moreRewards).forEach(::handleAdReward)
                    } else {
                        // Verification did not succeed — for example AdMob rejected the reward,
                        // the callback timed out, or a network error occurred. Do not grant the
                        // reward client-side as a fallback; let the user retry with another ad.
                    }
                }
            },
            trackingMetadata = RewardedAdTrackingMetadata(
                networkName = null,          // e.g., "Google Ads" (optional)
                mediatorName = AdMediatorName.AD_MOB,
                adFormat = AdFormat.REWARDED,
                placement = "home_screen",   // Your custom placement ID (optional)
                adUnitId = "AD_UNIT_ID",
                impressionId = impressionId,
            ),
        )
    })
}

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class)
fun handleAdReward(reward: VerifiedReward) {
    when (reward) {
        is VerifiedReward.VirtualCurrency -> {
            // RevenueCat already granted the reward server-side and invalidated the local
            // virtual currencies cache; the amount and currency come from the reward rule
            // you configured in the dashboard.
            println("Granted ${reward.amount} ${reward.code}")
        }
        is VerifiedReward.Entitlement -> {
            // RevenueCat already granted the entitlement server-side and refreshed customer
            // info before delivering this result — check entitlement status as usual.
            println("Granted entitlement ${reward.identifier} until ${reward.expiresAt}")
        }
        else -> Unit
    }
}
```

```dart
// Reward verification is experimental — the APIs may change.
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

RewardedAd.load(
  adUnitId: 'AD_UNIT_ID',
  request: const AdRequest(),
  rewardedAdLoadCallback: RewardedAdLoadCallback(
    onAdLoaded: (ad) async {
      // Use the loaded ad's response ID as the impression ID, then generate a
      // verification token and attach it as AdMob server-side verification
      // options before presenting.
      final impressionId = ad.responseInfo?.responseId ?? '';
      _token = await Purchases.generateRewardVerificationToken(impressionId);
      await ad.setServerSideOptions(ServerSideVerificationOptions(
        userId: _token!.appUserID,
        customData: _token!.customData,
      ));
      _rewardedAd = ad;
      _impressionId = impressionId;
    },
    onAdFailedToLoad: (error) => _rewardedAd = null,
  ),
);

// Later, present the ad. When the user earns the reward, poll RevenueCat for
// the verified result using the token's client transaction ID. Pass
// trackingMetadata so the SDK tracks the reward funnel (earned, verified,
// granted, failed to verify) the same way the AdMob adapter does
// automatically; omit it to poll without tracking.
_rewardedAd?.show(
  onUserEarnedReward: (ad, _) async {
    final result = await Purchases.pollRewardVerification(
      _token!.clientTransactionId,
      trackingMetadata: RewardedAdTrackingMetadata(
        networkName: null, // e.g., "Google Ads" (optional)
        mediatorName: AdMediatorName.adMob,
        adFormat: AdFormat.rewarded,
        placement: 'home_screen', // Your custom placement ID (optional)
        adUnitId: 'AD_UNIT_ID',
        impressionId: _impressionId,
      ),
    );

    if (result.failed || result.reward == null) {
      // Verification did not succeed — for example AdMob rejected the reward,
      // the callback timed out, or a network error occurred. Do not grant the
      // reward client-side as a fallback; let the user retry with another ad.
      return;
    }

    // An ad can grant more than one reward; handle the primary reward and any others the same way.
    for (final reward in [result.reward!, ...result.moreRewards]) {
      handleAdReward(reward);
    }
  },
);

void handleAdReward(VerifiedReward reward) {
  if (reward is VerifiedVirtualCurrencyReward) {
    // RevenueCat already granted the reward server-side and invalidated the local
    // virtual currencies cache; the amount and currency come from the reward rule
    // you configured in the dashboard.
    print('Granted ${reward.amount} ${reward.code}');

    // Refetch balances only if your UI needs the updated total.
    Purchases.getVirtualCurrencies();
  } else if (reward is VerifiedEntitlementReward) {
    // RevenueCat already granted the entitlement server-side and refreshed customer
    // info before delivering this result — check entitlement status as usual.
    print('Granted entitlement ${reward.identifier} until ${reward.expiresAt}');
  }
}
```

```jsx
// Reward verification is experimental — the APIs may change.
import { RewardedAd, RewardedAdEventType } from "react-native-google-mobile-ads";
import Purchases, { VerifiedReward } from "react-native-purchases";

// react-native-google-mobile-ads doesn't expose AdMob's response id before the
// ad loads (SSV options must be set at request time), so assign your own unique
// impression ID. Reuse it for your RevenueCat ad-tracking calls to correlate.
const impressionId = "YOUR_UNIQUE_IMPRESSION_ID";
const token = await Purchases.generateRewardVerificationToken(impressionId);

const rewarded = RewardedAd.createForAdRequest("AD_UNIT_ID", {
  serverSideVerificationOptions: {
    userId: token.appUserID,
    customData: token.customData,
  },
});

rewarded.addAdEventListener(RewardedAdEventType.LOADED, () => rewarded.show());

// When the user earns the reward, poll RevenueCat for the verified result
// using the token's client transaction ID. Pass trackingMetadata so the SDK
// tracks the reward funnel (earned, verified, granted, failed to verify) the
// same way the AdMob adapter does automatically; omit it to poll without tracking.
rewarded.addAdEventListener(RewardedAdEventType.EARNED_REWARD, async () => {
  const result = await Purchases.pollRewardVerification(token.clientTransactionId, {
    networkName: null, // e.g., "Google Ads" (optional)
    mediatorName: AdMediatorName.adMob,
    adFormat: AdFormat.rewarded,
    placement: "home_screen", // Your custom placement ID (optional)
    adUnitId: "AD_UNIT_ID",
    impressionId,
  });

  if (result.failed || !result.reward) {
    // Verification did not succeed — for example AdMob rejected the reward,
    // the callback timed out, or a network error occurred. Do not grant the
    // reward client-side as a fallback; let the user retry with another ad.
    return;
  }

  // An ad can grant more than one reward; handle the primary reward and any others the same way.
  for (const reward of [result.reward, ...result.moreRewards]) {
    handleAdReward(reward);
  }
});

rewarded.load();

function handleAdReward(reward: VerifiedReward) {
  switch (reward.type) {
    case "virtual_currency":
      // RevenueCat already granted the reward server-side and invalidated the local
      // virtual currencies cache; the amount and currency come from the reward rule
      // you configured in the dashboard.
      console.log(`Granted ${reward.amount} ${reward.code}`);

      // Refetch balances only if your UI needs the updated total.
      Purchases.getVirtualCurrencies();
      break;
    case "entitlement":
      // RevenueCat already granted the entitlement server-side and refreshed customer
      // info before delivering this result — check entitlement status as usual.
      console.log(`Granted entitlement ${reward.identifier} until ${reward.expiresAt}`);
      break;
  }
}
```

```cpp
// Experimental API — may change in a future release
using GoogleMobileAds.Api;

RewardedAd rewardedAd;
string clientTransactionId;
string impressionId;

void LoadRewardedAd()
{
    RewardedAd.Load("AD_UNIT_ID", new AdRequest(), (RewardedAd ad, LoadAdError error) =>
    {
        if (error != null || ad == null) return;

        // Use the loaded ad's response ID as the impression ID, generate a token,
        // and attach it to AdMob's server-side verification options before showing.
        var responseId = ad.GetResponseInfo()?.GetResponseId() ?? "";
        purchases.GenerateRewardVerificationToken(responseId, (token, tokenError) =>
        {
            if (tokenError != null || token == null) return;

            ad.SetServerSideVerificationOptions(new ServerSideVerificationOptions.Builder()
                .SetUserId(token.AppUserID)
                .SetCustomData(token.CustomData)
                .Build());

            rewardedAd = ad;
            clientTransactionId = token.ClientTransactionId;
            impressionId = responseId;
        });
    });
}

// Later, show the ad. When the user earns the reward, poll RevenueCat for the
// verified result using the token's client transaction ID. Pass trackingMetadata
// so the SDK tracks the reward funnel (earned, verified, granted, failed to
// verify) the same way the AdMob adapter does automatically; omit it to poll
// without tracking.
void ShowRewardedAd()
{
    rewardedAd.Show((Reward reward) =>
    {
        var trackingMetadata = new RewardedAdTrackingMetadata(
            AdTracker.MediatorName.AdMob,
            AdTracker.Format.Rewarded,
            "AD_UNIT_ID",
            impressionId,
            networkName: null,      // e.g., "Google Ads" (optional)
            placement: "home_screen" // Your custom placement ID (optional)
        );
        purchases.PollRewardVerification(clientTransactionId, (result, error) =>
        {
            if (error != null || result.Failed || result.Reward == null)
            {
                // Verification did not succeed — for example AdMob rejected the reward,
                // the callback timed out, or a network error occurred. Do not grant the
                // reward client-side as a fallback; let the user retry with another ad.
                return;
            }

            // An ad can grant more than one reward; handle the primary reward and any others the same way.
            HandleAdReward(result.Reward);
            foreach (var extraReward in result.MoreRewards)
            {
                HandleAdReward(extraReward);
            }
        }, trackingMetadata);
    });
}

void HandleAdReward(Purchases.VerifiedReward reward)
{
    switch (reward)
    {
        case Purchases.VerifiedReward.VirtualCurrency virtualCurrency:
            // RevenueCat already granted the reward server-side and invalidated the local
            // virtual currencies cache; the amount and currency come from the reward rule
            // you configured in the dashboard.
            Debug.Log($"Granted {virtualCurrency.Amount} {virtualCurrency.Code}");
            break;
        case Purchases.VerifiedReward.Entitlement entitlement:
            // RevenueCat already granted the entitlement server-side and refreshed customer
            // info before delivering this result — check entitlement status as usual.
            Debug.Log($"Granted entitlement {entitlement.Identifier} until {entitlement.ExpiresAt}");
            break;
    }
}
```

```kotlin
// Reward verification is experimental — the APIs may change.
// Google Mobile Ads has no Kotlin Multiplatform artifact, so loading and presenting the ad itself
// stays platform-specific (an `expect`/`actual` pair backed by play-services-ads on Android and the
// GoogleMobileAds SDK on iOS). The calls below are the common part shared by both.

// After the ad loads, generate a token and attach it to AdMob's server-side verification options
// through your platform-specific ad controller.
val token = Purchases.sharedInstance.generateRewardVerificationToken(impressionId)
adController.setServerSideVerificationOptions(userId = token.appUserID, customData = token.customData)

// Later, present the ad. When the user earns the reward, poll RevenueCat for the verified result
// using the token's client transaction ID. Pass trackingMetadata so the SDK tracks the reward
// funnel (earned, verified, granted, failed to verify) the same way the AdMob adapter does
// automatically; omit it to poll without tracking.
adController.present(
    onUserEarnedReward = {
        Purchases.sharedInstance.pollRewardVerification(
            clientTransactionId = token.clientTransactionId,
            onCompleted = { result ->
                val reward = result.verifiedReward
                if (result.failed || reward == null) {
                    // Verification did not succeed — for example AdMob rejected the reward, the
                    // callback timed out, or a network error occurred. Do not grant the reward
                    // client-side as a fallback; let the user retry with another ad.
                    return@pollRewardVerification
                }

                // An ad can grant more than one reward; handle the primary reward and any others the same way.
                (listOf(reward) + result.moreRewards).forEach(::handleAdReward)
            },
            trackingMetadata = RewardedAdTrackingMetadata(
                networkName = null,          // e.g., "Google Ads" (optional)
                mediatorName = AdMediatorName.AD_MOB,
                adFormat = AdFormat.REWARDED,
                placement = "home_screen",   // Your custom placement ID (optional)
                adUnitId = "AD_UNIT_ID",
                impressionId = impressionId,
            ),
        )
    }
)

fun handleAdReward(reward: VerifiedReward) {
    when (reward) {
        is VerifiedReward.VirtualCurrency -> {
            // RevenueCat already granted the reward server-side and invalidated the local
            // virtual currencies cache; the amount and currency come from the reward rule
            // you configured in the dashboard.
            println("Granted ${reward.amount} ${reward.code}")
        }
        is VerifiedReward.Entitlement -> {
            // RevenueCat already granted the entitlement server-side and refreshed customer
            // info before delivering this result — check entitlement status as usual.
            println("Granted entitlement ${reward.identifier} until ${reward.expiresAt}")
        }
        else -> Unit
    }
}
```

Rewarded interstitial ads work identically — use `RewardedInterstitialAd` in place of `RewardedAd`.

## Checking entitlement status

An entitlement granted from an ad reward is no different from one granted by a subscription — it's an active [entitlement](https://www.revenuecat.com/docs/getting-started/entitlements) in [`CustomerInfo`](https://www.revenuecat.com/docs/customers/customer-info) with an expiration date. RevenueCat doesn't notify your app the moment it expires, so apply the same rule you already follow for subscriptions: your app decides when to check status, not RevenueCat.

Check the entitlement's [active status](https://www.revenuecat.com/docs/customers/customer-info#checking-if-a-user-is-subscribed) at the point where you gate access, not once when the reward is granted and never again. A user who unlocks 30 minutes of premium access can leave the relevant screen open well past that window — if you only checked at grant time, they'd keep access after it should have ended.

See [Getting Subscription Status](https://www.revenuecat.com/docs/customers/customer-info) for the full set of options for checking and caching entitlement status.

## Granted reward events

Rewards earned in debug builds are treated as sandbox events.

For in-app currency, confirm a verified grant in the [Customer History](https://www.revenuecat.com/docs/dashboard-and-metrics/customer-profile) timeline, where it appears as an in-app currency transaction:

![Customer History timeline showing a granted in-app currency from an ad reward](https://www.revenuecat.com/docs_images/ad-monetization/customer-history-ad-reward.png)

Opening the entry shows a `VIRTUAL_CURRENCY_TRANSACTION` event whose `"source"` is `"ad_reward"`, which distinguishes ad-reward grants from purchases and other sources. The event also includes an `ad_transaction_id` matching the reward verification.

For entitlements, the grant is active as soon as verification succeeds and shows up in the customer's active entitlements — there's no separate event in Customer History yet.
