Integrate rewarded video in under 15 minutes.
A complete guide to the AppLixir HTML5 SDK. Zero dependencies, works in every modern browser, and ships with built-in TCF 2.3 consent handling. Vanilla JS, React, Vue, Phaser — all supported out of the box.
The HTML5 SDK is distributed as a single <script> tag — no npm package, no
build step, no framework lock-in. It works with vanilla JS, React, Vue, Phaser,
Construct 3, or any engine that outputs to canvas or DOM. Pin to v6 to
receive non-breaking updates automatically.
Server-side header bidding is now live for every impression. A real-time
auction runs across our demand stack before any AdX fallback — CPM lift, no
code changes. Existing integrations get the benefit by bumping the SDK URL to
v6.1.0.
Just browsing? The Quick start gets you to a working ad in 3 minutes. This page is the complete reference.
Installation
Add the SDK script to the <head> of every page that renders rewarded video.
The CDN is globally cached and versioned.
<script
src="https://cdn.applixir.com/applixir.app.v6.1.0.js"
async
></script>
The SDK exposes a single global, initializeAndOpenPlayer, attached to
window once loaded.
Grab a runnable example from applixir-integration/examples (HTML5, React, React Native, Phaser, Unity), or paste a copy-paste AI prompt into Claude Code / Codex / Cursor and it implements the integration for you.
Integration flow
Four steps from idle game to verified, monetized reward.
-
Pause the game loop
Before calling the SDK, pause audio, input, and rendering. The ad plays as a full-screen overlay — anything behind it wastes CPU and can cause desync bugs when the player returns.
-
Call
initializeAndOpenPlayer()Pass your API key, anchor element ID, and the
adStatusCallbackFnthat handles ad lifecycle events. -
Grant the reward when
status.type === "complete"The callback receives an object, not a string. Grant the reward only when
status.typeis"complete"— never on"skipped","manuallyEnded","consentDeclined", or any intermediate event. -
Resume the game
Re-check window focus and audio context state — browsers may suspend audio during the overlay.
Keep reward-granting code exclusively inside the status.type === "complete" branch of your callback. Client-side flags like rewardEarned = true set before the ad starts can be exploited by users closing the tab mid-video. For high-value rewards, also verify server-side via the Web Callback (see Handling status events correctly below).
Parameters
Every option accepted by initializeAndOpenPlayer(options).
applixir-ad-container.{`{ type, ad?, error? }`}. The full list of type values and the correct way to grant a reward is below — see Handling status events correctly.getError().data contains type, errorCode, errorMessage, and innerError.Handling status events correctly
The reward should ultimately come from a server-side webhook, not the
client. AppLixir sends a signed GET to your backend on every
complete event — that's the source of truth for crediting a player's
account. The client-side status.type === "complete" is for optimistic
UX only (instant "Reward earned!" toast). If you're building a real
economy, you need both halves of this section — the client handler below
and the server webhook further down.
This is the #1 source of integration tickets — almost every "the callback
never fires" report comes from treating status as a string. It's an object.
// What the SDK actually passes to adStatusCallbackFn:
{ type: "complete" | "allAdsCompleted" | "started" | "loaded"
| "firstQuartile" | "midpoint" | "thirdQuartile"
| "click" | "paused" | "skipped"
| "manuallyEnded" | "consentDeclined",
ad?: { adId, isLinear, duration, title, adSystem }, // present when relevant
error?: string } // present on AD_ERROR
The reward grant rule
Grant the reward only when status.type === "complete". That single event
means the user watched the full ad through to the end.
adStatusCallbackFn: (status) => {
if (status.type === "complete") {
grantReward();
}
}
Events to ignore for reward purposes
type | What it means | Reward? |
|---|---|---|
loaded · started · firstQuartile · midpoint · thirdQuartile | Ad is mid-playback | ❌ Premature — user can still skip |
complete | User watched the entire ad | ✅ Grant here |
allAdsCompleted | Ads manager is done — fires regardless of skip/complete | ❌ Don't reward here; use complete |
skipped · manuallyEnded | User dismissed before completion | ❌ |
consentDeclined | User refused TCF/CMP consent | ❌ |
click · paused | Interaction event | ❌ |
Common mistakes
- Treating
statusas a string.if (status === "complete")is alwaysfalse— the argument is an object. Usestatus.type. - Granting on
allAdsCompleted. Fires even when the user skipped. Usecomplete, which only fires after full playback. - Granting at
startedor a quartile. The user can still skip or close the tab. Wait forcomplete. - Inverted boolean flags. Setting
rewardEarned = truebefore the ad plays and then conditionally clearing it onskippedis fragile — tab close, network error, and AD_ERROR all bypass the cleanup. The reverse pattern (defaultfalse, settrueonly inside thecompletebranch) is safer.
Going to production: server-side verification is required
Before launching a real economy you must configure the Web Callback in
your AppLixir dashboard. The client is fully untrusted: DevTools can fire
arbitrary callbacks, browser extensions can replay events, multi-tab
sessions can double-fire complete for the same impression, and a
motivated user will find every one of those paths. Granting persistent
currency or items purely from adStatusCallbackFn is exploitable.
How it works: AppLixir sends a signed GET to your backend each
time an ad completes. Your backend is the only place that
credits the player; the client-side complete event is just for the
optimistic UI. See
Step 4: Setting up callbacks
for the signature formula, verify snippets, and dashboard setup.
Idempotency. In MD5 and TID mode your backend should dedupe on the
tid — a retry, network hiccup, or replay shouldn't double-credit the
player. Treat the webhook as at-least-once delivery.
Reconciliation. If the optimistic UI fired but no webhook arrives
within a few seconds (consent block, geo issue, network drop after
complete), refresh from server state so the user doesn't see a phantom
reward. Don't roll back the UI from the client — let the server be
authoritative.
Pass userId and customData in your options so the webhook can
attribute the reward to the right player:
const options = {
apiKey: "xxxx-xxxx-xxxx-xxxx",
injectionElementId: "applixir-ad-container",
userId: "player-7890", // surfaces in the web callback
customData: { sessionId, levelId }, // arbitrary JSON, echoed back
adStatusCallbackFn: (status) => {
if (status.type === "complete") optimisticRewardUi();
},
};
Handling errors
Errors arrive separately on adErrorCallbackFn, not on the status callback.
The error object exposes getError().data with { type, errorCode, errorMessage, innerError }:
adErrorCallbackFn: (err) => {
const { type, errorCode, errorMessage } = err.getError().data;
console.warn(`[AppLixir] ${type} (code ${errorCode}): ${errorMessage}`);
// Never grant a reward in here.
}
The full enumerated type list (38 VAST/IMA error codes) lives in
Step 4: Setting up callbacks.
Preload for instant reveal
By default, the click-to-reveal sequence (auction → VAST → creative load) runs serially off the user's click — typically 1.5–3.5s before the ad appears. For instant reveal (~100–300ms), preload the ad ahead of the click on a high-intent signal.
// On a high-intent signal: reward modal mount, level complete,
// "watch ad to double coins" button render — NOT on page load.
const ad = await preloadAd({
apiKey: "xxxx-xxxx-xxxx-xxxx",
injectionElementId: "applixir-ad-container",
adStatusCallbackFn: (status) => {
if (status.type === "complete") grantReward();
},
});
// On the click — ad is already in memory, reveal is near-instant.
watchButton.addEventListener("click", () => ad.show());
preloadAd(options) takes the same options as initializeAndOpenPlayer and
returns { show, application }. Each handle is single-shot — re-call
preloadAd() after each show() to park the next ad.
On a discrete intent signal, not on page load. Page-load preloads waste ad calls on visitors who never click; on-click is the slow path we're trying to escape. The middle row — preload on intent — is what Poki, CrazyGames, and Google H5 Games Ads do under the hood.
Caveats to handle
- Bid TTL ~5 min. Prebid video bids expire; if more than ~4 min pass
between
preloadAd()andshow(), re-callpreloadAd()to refresh. - Errors reject the promise. No-fill, VAST errors, and network failures
cause
preloadAd()to reject. Wrap intry/catchand either fall back toinitializeAndOpenPlayer()for one more best-effort try, or show a polite no-ad message. - Incognito / 3p-cookie blocks can fail the auction silently. Always keep
an on-click fallback path live (it's safer to call
initializeAndOpenPlayer()when the handle is missing than to assume the preload succeeded).
Persistent "Watch Ad" buttons — keep-alive pattern
For UIs where the watch button is always visible (reward HUD, "double coins" CTA), there's no discrete intent signal — the button itself is the permanent intent. Keep an ad parked while the button is visible:
let adHandle = null;
let lastRefresh = 0;
async function refreshAd() {
try {
adHandle = await preloadAd(options);
lastRefresh = Date.now();
} catch (e) {
adHandle = null;
}
}
refreshAd(); // first render
setInterval(refreshAd, 4 * 60 * 1000); // stay ahead of TTL
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible"
&& Date.now() - lastRefresh > 60_000) refreshAd();
});
watchBtn.addEventListener("click", () => {
if (adHandle) adHandle.show();
else initializeAndOpenPlayer(options); // slow-path fallback
});
Cost: ~7 ad calls per 30-min session even on a non-clicker. Fine for high-DAU
publishers. For low-DAU, replace the 4-min keep-alive with a touchstart/
hover preload on the button — fires ~50–200ms before the click for a partial
latency win without the polling cost.
Common errors
These cover the majority of integration issues we see in support tickets.
TypeError: initializeAndOpenPlayer is not a function
Cause. Calling the SDK before the script finishes loading.
Fix. Remove async from the script tag, or wrap your call in window.addEventListener("load", ...).
AppLixir: domain not authorized
Cause. The domain serving the SDK isn't registered in your AppLixir dashboard.
Fix. Dashboard → Sites → add your exact domain. Localhost won't match — deploy to a real domain for testing.
ReferenceError: Didomi is not defined
Cause. The Didomi CMP failed to persist across a page navigation — common on SPAs that don't trigger full reloads.
Fix. Re-initialize the CMP after route changes, or manage consent yourself via the AppLixir dashboard consent settings.
Low fill rate (less than 70%)
Cause. Usually unsupported geography, content category mismatch, or a new site that hasn't warmed up.
Fix. New sites typically stabilize after ~50k impressions. Make sure your ads.txt is correct and GAM propagation is complete.
Questions? Email support@applixir.com.