casperlaitw/laravel-admob-ssv
Laravel AdMob SSV adds server-side verification for Google AdMob rewarded ad callbacks. It validates SSV signatures and callback parameters so you can securely confirm ad views and award user rewards. Includes simple configuration, key management, and robust validation for production reward flows.
This package handles Google AdMob Server-Side Verification (SSV) callbacks: AdMob calls a URL on your app after a user earns a reward, and the package verifies Google's signature before letting you act on it. v1.5.0 is a compatibility-only release (adds Laravel 12 and 13 support), so the day-to-day workflow is unchanged — it just means you can now composer update this package on modern Laravel skeletons.
composer require casperlai/laravel-admob-ssv
php artisan vendor:publish --tag=admob-ssv-config
config/admob-ssv.php first — this is where the callback route, middleware group, and any key/URL settings live. Decide whether the package registers its own route or whether you point it at a route you own./admob-ssv/callback by default) and paste it into AdMob → Ad unit → Server-side verification (or the account-level SSV URL) in the AdMob console.EventServiceProvider (or use Event::listen), and inside it read custom_data to identify the user, reward_amount/reward_item to know what to grant, and transaction_id as the idempotency key.If you just need the signature check, you're done at step 3 — the package rejects bad signatures for you.
Event-listener as the integration point. The idiomatic pattern is: package verifies → package dispatches event → your listener does domain work. Keep the listener thin and push the actual grant into a queued job or an action class so a slow DB doesn't time out Google's callback (Google expects a fast 200).
Event::listen(AdMobSsvVerified::class, function ($event) {
RewardGrant::dispatch($event->transactionId, $event->customData, $event->rewardAmount);
});
Identify the user via custom_data, not user_id. user_id is whatever you set in the AdMob SDK and can be absent or spoofable-looking; custom_data is the field you control per-request. Common pattern: put an opaque, signed token (or the user's ULID + a HMAC) in custom_data from the app, then look the user up server-side. Never trust a bare user ID from the query string to authorise a grant.
Idempotency is mandatory. AdMob retries callbacks. Wrap the grant in a unique index on transaction_id (or a reward_transactions table with firstOrCreate) so a retry doesn't double-credit. This is the single most important pattern with this package — the signature check proves authenticity, not uniqueness.
Environment-parity config. Keep the SSV URL per environment: a staging app in AdMob pointing at staging.example.com and production pointing at example.com. Because the URL lives in the AdMob console (not your repo), document it in your README so nobody's surprised when staging callbacks land in prod after a config copy.
Testing locally. Expose your dev server (ngrok http 80, valet share, or expose) and temporarily point a test ad unit's SSV URL at the tunnel. Note you can't easily forge a valid signature — so for unit tests, either fake the event (Event::fake()) or bypass verification in a test-only config flag rather than trying to mint Google signatures.
Multi-app / multi-ad-unit setups. If you run several apps through one Laravel install, use the ad_unit (and ad_network) fields in the payload to route to the right reward table or tenant. Combine with a rewarded_videos/reward_ledger table keyed by (ad_unit, transaction_id).
Pair with your own audit log. Persist the raw callback payload (minus signature) alongside the grant. When a user claims "I watched the ad and got nothing," this table is your only evidence, since you can't query AdMob for individual SSV callbacks.
TrimStrings/ConvertEmptyStringsToNull weirdness on that route) and don't add request-mutating middleware.gstatic.com. In locked-down networks (or CI), outbound HTTPS to www.gstatic.com must be allowed, and the package may cache keys — clear cache if you ever see valid callbacks rejected after a key rotation.timestamp field plus your transaction_id uniqueness constraint to reject stale replays; don't rely on the package to do it.custom_data has length limits and URL-encoding quirks. Keep it short and URL-safe; long JSON blobs may be truncated or mangled. If you need richer payloads, store the context server-side keyed by a short token.custom_data tokens into logs. Redact.composer.json laravel/framework constraint resolves and re-run the SSV test flow once, since Laravel 13 may change how route middleware groups are resolved.How can I help you explore Laravel packages today?