SofAds SDK reference
The SofAds TV SDK shows interstitial, rewarded and QR ads in Smart TV apps and games on Samsung Tizen, LG webOS, Whale TV and Titan OS, and sends free usage analytics. It renders every ad as an overlay above your app, handles the remote while the ad is up, and never throws: every call settles within a fixed time.
The platform pages cover the files each TV needs: Tizen, webOS, Whale TV and Titan OS.
Install
Script tag. Bundle sofads.min.js in your app package (recommended for TV store builds), or load a pinned version from sdk.sofadsrv.com. The script defines the global SofAds.
<script src="js/sofads.min.js"></script>
<!-- or a pinned CDN version, never a moving "latest": -->
<!-- <script src="https://sdk.sofadsrv.com/v1.0.0/sofads.min.js"></script> -->npm. The package ships ESM, CommonJS and TypeScript types:
npm install @sofads/tv-sdkimport SofAds from "@sofads/tv-sdk";
SofAds.init({ appId: "sof_YOUR_APP_ID" });The SDK has no runtime dependencies and runs on Chrome 53 and later (ES2015), which covers TVs from 2017 on.
Quick start
SofAds.init({
appId: "sof_YOUR_APP_ID",
onPause: () => game.pause(), // stop your render loop before an ad plays
onResume: () => game.resume(), // always follows onPause
});
// At a natural break. The call always settles; your game just continues.
async function onLevelComplete() {
await SofAds.showInterstitial({ placement: "level_break" });
startNextLevel();
}
// The user chose to watch for a reward. addCoins is your own function.
function onExtraLifeButton() {
SofAds.showRewarded({
placement: "extra_life",
onReward: (reward) => addCoins(reward.amount),
});
}You don't need to wait for init before showing an ad: a show call made while init is still running waits for it, within its own timeout. Every call returns a promise, so you can await it or just call it.
init(options)
SofAds.init(options) starts the SDK and returns a promise of { status, platform, sdk, bootstrap?, reason? }, where status is "ready", "error" or "timeout". It is safe to call without await and calling it twice has no effect. It resolves within 5 seconds, also when the network is down.
| Option | Default | What it does |
|---|---|---|
appId | (required) | Your app ID from the dashboard: sof_ followed by 10 characters. An invalid ID makes init resolve error, and every show resolves error. |
onPause, onResume | none | Renderer handoff. Stop your render loop (Lightning, Blits, Pixi, a canvas loop) in onPause so video plays smoothly, and restart it in onResume. |
consent | {} | Consent known at start-up: { gdpr, tcf, gpp, gpp_sid, us_privacy, analytics, ads }. See consent. |
test | false | Ask for test ads only. |
debug | false | Console logging. SofAds.debug(true) also shows the debug overlay. |
theme | { accent: "#7c5cff" } | The one accent color of the ad UI (focused button, QR frame). Hex, rgb() or hsl(). |
prefetch | none | [{ placement, type }]: warm up an ad for these placements right after init. |
platform | detected | Force a platform. Use "web" only for a desktop or demo build; the SDK never guesses it. |
appVersion, nativeAppId | read on Tizen and webOS | Your app version and store app ID, reported with requests and analytics. |
whale | none | Whale TV: { getAdId } returns the WAID once you can read it. See Whale TV setup. |
titan | none | Titan OS: { getDeviceInfo } from the Titan SDK. See Titan OS setup. |
tmax | 1500 | Time budget of the ad server in ms (100 to 5000). |
endCardTimeoutSeconds | 0 | 0 keeps the end card until the user presses OK (so they can scan); a number closes it after that many seconds. |
container | document.body | The element the ad overlay mounts into. |
SofAds.init({
appId: "sof_YOUR_APP_ID",
consent: { gdpr: 1, tcf: "CP_your_tcf_string" },
theme: { accent: "#1f8efd" },
prefetch: [{ placement: "extra_life", type: "rewarded" }],
});Showing ads
showInterstitial
SofAds.showInterstitial({ placement }) shows a full-screen ad at a natural break: between levels, after a game over, before the next episode. It resolves when the ad is gone, or right away with no_fill when there is nothing to show.
async function betweenLevels() {
const r = await SofAds.showInterstitial({ placement: "level_break" });
if (r.status === "shown") {
// The ad was on screen; r.completed and r.skipped tell you how it ended.
}
startNextLevel();
}showRewarded
SofAds.showRewarded({ placement, reward?, onReward }) shows an ad the user chose to watch for a reward. Grant the reward in onReward: it is called exactly once, and only when the reward is earned (the video completed, or the QR code was scanned on a scan-to-reward ad). Closing early means no reward, and the screen says so.
SofAds.showRewarded({
placement: "extra_life",
onReward: (reward) => addCoins(reward.amount), // addCoins is your own function
});Rewarded ads are never skippable. On a scan-to-reward ad the QR card says "Scan to get +500 [icon]", polls for the scan and calls onReward the moment it is confirmed; the user then closes the ad with Continue.
ShowResult
Every show call resolves with the same object. Most apps never need to read it.
| Field | Meaning |
|---|---|
status | "shown", "no_fill", "error" or "timeout". |
placement | The placement you asked for. |
completed | The ad was watched or displayed to the end. |
skipped | The user skipped (interstitial) or closed early (rewarded). |
rewarded | onReward was called for this ad. |
test | It was a test ad. |
format, ad_id | The format shown (vast, qr_card, ...) and an opaque ID for support tickets. |
reason, error_code | Diagnostics only, e.g. no_inventory or a VAST error code. |
Rewards
The publisher owns the reward. You set it per placement in the dashboard (an amount, an icon and an optional name per language), and the ad screen shows it as +500 [icon]. The advertiser never writes reward text.
onReward receives the reward the user was shown:
| Field | Meaning |
|---|---|
amount | The amount the screen promised, e.g. 500. Absent when the placement has no amount and you passed none. |
name | The reward name in the ad's language, when there is one. |
reward | The full reward: { amount?, name?, icon_url? }. Absent when neither the placement nor your call set a reward (the screen then said "your reward"). |
placement, ad_id | Which placement and which ad. |
reward_on | "complete" (watched to the end) or "scan" (scan-to-reward). |
Override the amount or name for one call, for example when the reward depends on the level. The icon always comes from the placement settings:
function offerDoubleCoins(levelCoins) {
SofAds.showRewarded({
placement: "level_end",
reward: { amount: 2 * levelCoins, name: "coins" },
onReward: (reward) => addCoins(reward.amount || 2 * levelCoins),
});
}Events
SofAds.on(event, fn) subscribes to an event and returns a function that unsubscribes. once and off work as you'd expect. A listener that throws never breaks the SDK.
| Event | When |
|---|---|
ready | init finished. |
show | An ad overlay opened. |
impression | The ad was counted as seen. |
complete | The ad played or displayed to the end. |
skip | The user skipped (see skippable ads). |
close | The overlay closed. Always follows show. |
reward | A reward was granted (onReward was called). |
no_fill | There was no ad for the request. |
error | An ad failed; the call resolved error. |
const stop = SofAds.on("close", () => music.resume());
SofAds.once("ready", () => console.log("SofAds is ready"));
// Later, when the listener is no longer needed:
stop();Custom analytics events go through SofAds.track(name, props?). Names use lowercase letters, digits and underscores (up to 48 characters), with up to 20 flat properties:
SofAds.track("level_complete", { level: 3, stars: 2 });Consent
The SDK follows the consent your app collects. Pass what you know at start-up in init({ consent }), and call SofAds.setConsent() whenever it changes: the change is merged into the current state and applies from the next request. You may call it before init, for example when your consent tool answers early.
// From your consent tool (IAB TCF v2):
SofAds.setConsent({ gdpr: 1, tcf: "CP_your_tcf_string" });
// Or your own consent screen, without TCF:
SofAds.setConsent({ ads: false, analytics: true });| Field | Meaning |
|---|---|
gdpr | 1 when GDPR applies, 0 when it doesn't. Leave it out to let the ad server decide from the user's location. |
tcf | An IAB TCF v2 consent string. |
gpp, gpp_sid, us_privacy | US privacy signals (GPP and the older US Privacy string). |
ads | Explicit consent to personalized ads and device IDs. Without it, under GDPR, TCF purpose 1 decides. |
analytics | false runs analytics without a persistent ID (cookieless mode). |
Under GDPR the SDK sends no advertising ID unless ads is true or the TCF string allows it, and without analytics consent it stores nothing for analytics. For a "Reset advertising ID" button in your privacy panel, call SofAds.resetAdvertisingId(): it creates a new SofAds install ID (the TV's own advertising ID is reset in the TV's settings).
Test mode
Until your store listing is live, your app gets test ads only: real ad screens, labeled "Test ad", never billed. A development build can ask for them at any time:
SofAds.init({ appId: "sof_YOUR_APP_ID", test: true });The SDK logs one console line explaining why it serves test ads (listing pending, in review, or a registered test device), and ShowResult.test is true for test ads. Everything about test ads, the scan test and test devices is in Test ads and test devices.
Debug overlay
SofAds.debug(true) shows a panel at the bottom left of the screen, above your app but never taking input: the SDK version, the install ID as text and as a QR code (to register the TV as a test device), the app and platform, the listing status, test mode and its reason, signal providers, integration warnings with a link to the fix, and a link to these docs. SofAds.debug(false) removes it. You can call it any time, also before init.
SofAds.debug(true);SofAds.getInfo() returns the same information as an object, for your own logs.
Other calls
| Call | What it does |
|---|---|
SofAds.prefetch({ placement, type? }) | Warms up one ad for a placement and preloads its media. Resolves true when it has one. |
SofAds.closeAd() | Closes a visible ad at once, for example when your app is suspended. |
SofAds.isShowing() | true while an ad overlay is visible. |
SofAds.preview(container, ad, opts?) | Renders the real ad screen as a still frame, for dashboards and creative reviews. No network calls or tracking. |
document.addEventListener("visibilitychange", () => {
if (document.hidden && SofAds.isShowing()) SofAds.closeAd();
});Never-throw guarantees
- Every public call catches its own errors. Ads off, network down or odd firmware: your app carries on.
- Every show call resolves within a fixed time with
shown,no_fill,errorortimeout. closealways followsshow, andonResumealways followsonPause.- While an ad is visible it captures the remote keys, so your app never sees them; BACK works on every ad screen.