Skip to main content
SDK · HTML5Stable · v6.1.08 min read

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.

Typical CPM
$4+/mille
8× standard display
Integration
3 lines
zero dependencies
Fill rate · avg.
94%
across 31 partners

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.

What's new in v6.1

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.

tip

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.

Copy working code, or let an AI agent do it

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.

  1. 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.

  2. Call initializeAndOpenPlayer()

    Pass your API key, anchor element ID, and the adStatusCallbackFn that handles ad lifecycle events.

  3. Grant the reward when status.type === "complete"

    The callback receives an object, not a string. Grant the reward only when status.type is "complete" — never on "skipped", "manuallyEnded", "consentDeclined", or any intermediate event.

  4. Resume the game

    Re-check window focus and audio context state — browsers may suspend audio during the overlay.

Best practice

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).

apiKey string required
Your publisher API key. Find it in the dashboard under Sites. Store in an environment variable — don't hardcode in public repos.
injectionElementId string required
The id of the div where the ad player will be injected. Typically applixir-ad-container.
adStatusCallbackFn (status) => void required
Called on every ad lifecycle event. Receives an object {`{ type, ad?, error? }`}. The full list of type values and the correct way to grant a reward is below — see Handling status events correctly.
adErrorCallbackFn (err) => void optional
Fired on VAST, CMP, or network errors. Receives an object whose getError().data contains type, errorCode, errorMessage, and innerError.

Handling status events correctly

Production pattern

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

typeWhat it meansReward?
loaded · started · firstQuartile · midpoint · thirdQuartileAd is mid-playback❌ Premature — user can still skip
completeUser watched the entire adGrant here
allAdsCompletedAds manager is done — fires regardless of skip/complete❌ Don't reward here; use complete
skipped · manuallyEndedUser dismissed before completion
consentDeclinedUser refused TCF/CMP consent
click · pausedInteraction event

Common mistakes

  • Treating status as a string. if (status === "complete") is always false — the argument is an object. Use status.type.
  • Granting on allAdsCompleted. Fires even when the user skipped. Use complete, which only fires after full playback.
  • Granting at started or a quartile. The user can still skip or close the tab. Wait for complete.
  • Inverted boolean flags. Setting rewardEarned = true before the ad plays and then conditionally clearing it on skipped is fragile — tab close, network error, and AD_ERROR all bypass the cleanup. The reverse pattern (default false, set true only inside the complete branch) 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.

When to preload

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() and show(), re-call preloadAd() to refresh.
  • Errors reject the promise. No-fill, VAST errors, and network failures cause preloadAd() to reject. Wrap in try/catch and either fall back to initializeAndOpenPlayer() 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.