Step 4: Setting up callbacks
AppLixir delivers ad lifecycle information through two callbacks. They serve different purposes and you need both for a production integration.
| Callback | Where it runs | What it's for |
|---|---|---|
Local callback (adStatusCallbackFn) | In the browser, inside your page | Optimistic UI: dismiss the player, advance the game, queue a reward animation |
| Web callback | Server-to-server, AppLixir → your backend | Source of truth for granting persistent rewards (currency, items, XP) |
Rule of thumb: Use the local callback to drive what the player sees. Use the web callback to drive what the player gets.
Local callback
You wire the local callback into the adStatusCallbackFn option when you initialize the player (see Step 3). It receives a status object:
{ type: "allAdsCompleted" | "click" | "complete" | "firstQuartile" | "loaded" | "midpoint" | "paused" | "started" | "thirdQuartile" | "skipped" | "manuallyEnded" | "consentDeclined" }
Event types
- allAdsCompleted — ads manager is done playing all valid ads in the response, or the response returned no valid ads
- click — the ad was clicked
- complete — the ad finished playing (the one event the player actually watched it through)
- firstQuartile — playhead crossed the first quartile
- loaded — ad data is available
- midpoint — playhead crossed the midpoint
- paused — the ad is paused
- started — the ad started playing
- thirdQuartile — playhead crossed the third quartile
- skipped — the user skipped the ad
- manuallyEnded — the user manually ended the ad
- consentDeclined — the user declined consent for personalized ads
Using complete for optimistic UI
status.type === "complete" is the right hook to dismiss the player and show "Reward incoming…" — but it is not the right hook to grant a persistent reward. The browser is fully untrusted: DevTools can fire arbitrary callbacks, extensions can replay events, multi-tab sessions can double-fire complete for the same impression. Treat the local complete as optimistic UX only; let the web callback (below) be the system of record.
Errors
When the player surfaces an error, you can pull the details via getError() — it returns a type (one of ~38 VAST/IMA error strings), an errorCode, an errorMessage, and an innerError. The full enumerated list lives in the Local callback error codes reference.
Web callback
This is the server-to-server channel and is the only channel that should grant persistent rewards. When an ad completes, AppLixir sends an HTTP GET to a URL you provide, with the parameters described below.
Configuration
- In
client.applixir.com, open the Server-side reward webhook card. - Set the URL to an HTTPS endpoint on your backend that returns
200 OKfor the happy path. - Set the Secret to a long random string. Store the same secret on your backend — depending on the mode, you use it to verify the request signature.
- Choose the Mode (see below).
Modes
The Mode you pick on the dashboard controls what AppLixir adds to the request:
| Mode | What AppLixir adds | Use it for |
|---|---|---|
| No MD5 or TID | Nothing extra — verify via the secretKey param | Quick tests, low-stakes rewards |
| MD5 Only | An MD5 signature | Production — verify the signature before crediting |
| MD5 and TID | An MD5 signature and a unique tid (transaction ID) | Production with idempotency — verify + dedupe on tid |
MD5 and TID is recommended for production. Verify the signature rather than trusting the plaintext secretKey (which is deprecated for MD5 modes and will be removed).
Request format
Every request includes:
| Param | Always sent | Notes |
|---|---|---|
gameApiKey | yes | The game's API key |
gameId | yes | The game's ID |
userId | if you passed it | The userId from your player options (see below) |
customData | if you passed it | URL-encoded JSON. Not part of the signature |
secretKey | yes (all modes) | The raw secret, plaintext. On MD5 modes this is deprecated — verify signature instead; it will be removed for MD5 modes in a future version |
signature | MD5 Only and MD5 and TID | Lowercase hex MD5 (see formula) |
tid | MD5 and TID only | Unique per callback — dedupe on this |
Verifying the signature (MD5 modes)
Recompute the MD5 over the concatenation below using your stored secret, then compare to the signature param. If userId was not provided it is the empty string; in MD5 Only mode there is no tid segment.
signature = md5( gameApiKey + gameId + userId + tid + secret ) // MD5 and TID
signature = md5( gameApiKey + gameId + userId + secret ) // MD5 Only
PHP:
$expected = md5($_GET['gameApiKey'] . $_GET['gameId'] . ($_GET['userId'] ?? '') . ($_GET['tid'] ?? '') . $SECRET);
if (!hash_equals($expected, $_GET['signature'] ?? '')) { http_response_code(403); exit; }
Node:
import crypto from "crypto";
const q = req.query;
const expected = crypto.createHash("md5")
.update(`${q.gameApiKey}${q.gameId}${q.userId ?? ""}${q.tid ?? ""}${SECRET}`)
.digest("hex");
if (expected !== q.signature) return res.sendStatus(403);
Reward grant rule
Your backend is the system of record for rewards. The flow:
- Player watches the ad → AppLixir player fires
status.type === "complete"→ your client shows "Reward incoming…" - AppLixir sends the
GETto your webhook - Your backend verifies the signature (MD5 modes), dedupes on
tid(MD5 and TID), credits the player - Your client refreshes from server state and shows the granted reward
Idempotency
In MD5 and TID mode, treat the webhook as at-least-once delivery. Network retries and edge restarts can deliver the same tid more than once. Dedupe on tid before crediting — a second arrival for a tid you've already processed should be a no-op 200 OK, not a duplicate grant.
Reconciliation
If the optimistic UI fired complete but no webhook arrives within a few seconds (consent block, geo issue, network drop after complete), do not roll back the UI from the client. Refresh from server state instead — let the server be authoritative and the UI will reconcile itself.
Passing context to the webhook
Pass userId and customData in your player options so the webhook can attribute the reward to the right player:
initializeAndOpenPlayer({
apiKey: "YOUR-API-KEY",
userId: "player-7890",
customData: { level: 12, sessionId: "abc-123" },
// ... other options
});
userId surfaces as the userId param (and is part of the signature). customData surfaces as a URL-encoded JSON customData param.
Testing the callback
You can test the integration end-to-end with webhook.site:
- Open webhook.site. It auto-generates a unique URL for you.
- In the AppLixir dashboard, paste that URL in, set any temporary value as the secret, and choose No MD5 or TID (so signature verification isn't needed for the test).
- Run a rewarded ad in your game and wait for it to complete. The request should appear in webhook.site.
It may take a few attempts before the callback shows up — there's a short propagation delay after updating callback settings. Once active, requests come through consistently.
Production checklist
- Web callback URL is an HTTPS endpoint reachable from the public internet
- Mode is MD5 Only or MD5 and TID (not No MD5 or TID)
- Backend verifies the
signatureon every incoming request - Backend dedupes on
tid(MD5 and TID mode) - Backend credits the player only inside the web-callback handler (not on the local
completeevent) - Client UI uses the local
completefor optimistic feedback only, and refreshes from server state to confirm the reward - Reconciliation path exists for the "optimistic UI fired, no webhook arrived" case