SDK
The Yes SDK runs inside your game. Submit scores, draw leaderboards, reward players with ads or Tickets, show interstitials and trigger haptics — all from a single global yes object.
Getting Started
No installation needed
yes object.Quick Start
// Submit a score (no need to wait for it)yes.submitScore(score);// Revive the player for a rewarded adconst ad = await yes.watchRewardedAd('revive');if (ad.rewarded) revivePlayer();// Trigger haptic feedbackyes.haptic('success');
yes.onReady(callback)#yes.onReady(callback: () => void): void
Runs your callback once the SDK has started. If it already has, the callback runs straight away. You don't need it for getLanguage(), which is correct from your first line of code.
Example
yes.onReady(async () => {const player = await yes.getPlayer();showWelcome(player.name);});
yes.version#yes.version: string
The SDK version, e.g. '1.10.0'. Handy in bug reports.
Example
console.log(yes.version);
Calls Never Fail
Every async SDK call always resolves — it never rejects. When something goes wrong at runtime (the player is offline, an ad didn't load, the app didn't answer), you get a “didn't happen” value instead: rewarded: false, spent: false, validated: false, null. So you don't need try/catch; just check the result.
The one exception is a bug in your own code. Bad arguments — a score that isn't a number, an empty placement name, a Ticket amount outside 1–10 — throw straight away, so the mistake shows up where you made it.
const { spent } = await yes.spendTicket(3, 'booster');if (spent) grantBooster(); // otherwise just carry onyes.submitScore(NaN); // throws at once: fix your code
API Reference
Player
yes.getPlayer()#yes.getPlayer(): Promise<Player>
Get the current player as other players see them on leaderboards: a name and an avatar. There are no player ids. If the app is still signing the player in, the call waits for it. Both fields are null when nobody is signed in.
Returns
Promise<Player>{ name: string | null, avatarUrl: string | null }
Example
const player = await yes.getPlayer();nameLabel.textContent = player.name ?? 'Player';
Scores & Leaderboards
Boost player retention
yes.submitScore()#yes.submitScore(score: number, options?: SubmitScoreOptions): Promise<SubmitScoreResult>
Submit a score to the leaderboard. It never blocks your game, so you can call it and move on. If you want the result, await it: it resolves once the server has checked the score. Offline, the score is saved on the device and syncs later.
Parameters
| Name | Type | Description |
|---|---|---|
score | number | The score, a number of 0 or more. Anything else throws. |
options.metadata? | object | Extra data to store with the score, e.g. { level: 5 } |
Returns
Promise<SubmitScoreResult>isNewBest, rank and score; validated (true when the server confirmed it); snapshot, the fresh leaderboard (or null). Offline it resolves { validated: false, queued: true }.
Example
// Fire and forgetyes.submitScore(gameState.score);// Or wait for the server's answerconst result = await yes.submitScore(gameState.score);if (result.validated && result.isNewBest) {showBanner(`New best! Rank #${result.rank}`);}
yes.getLeaderboard()#yes.getLeaderboard(): Promise<LeaderboardSnapshot | null>
Get the game's leaderboards so you can draw your own board. You get two boards: weekly (resets every week) and allTime. Each has the top 10 (top), the player's own row (self, or null if they haven't scored) and the players just above and below them (neighbours). Calling it again is cheap, and calling it right after submitScore() gives you the board with the new score. Resolves null when there's no leaderboard to show, e.g. the player is signed out — skip your leaderboard UI then.
Returns
Promise<LeaderboardSnapshot | null>Rows are { name, avatarUrl, score, rank, isSelf, entryId }. weekly also has endsAt (when it resets) and now (server time), for a countdown. stale: true means it may be missing the latest score (e.g. offline).
Example
const board = await yes.getLeaderboard();if (board) {const me = board.weekly.self;showRank(me ? `#${me.rank} this week` : 'Play to get on the board');for (const row of board.weekly.top) {drawRow(row.rank, row.name, row.score, row.isSelf);}}
Reward Placements
Your game owns the offer
Every call takes a
placement name (e.g. 'revive'): a non-empty string of at most 64 characters. Use the same name for one spot every time.yes.isRewardedAdReady()#yes.isRewardedAdReady(): Promise<RewardState>
Read what your offer needs to draw itself: whether a rewarded ad is ready, and how many Tickets the player holds.
Returns
Promise<RewardState>{ ready: boolean, ticketBalance: number }. { ready: false, ticketBalance: 0 } if the app can't answer.
Example
const { ready, ticketBalance } = await yes.isRewardedAdReady();watchAdButton.disabled = !ready;ticketLabel.textContent = `You have ${ticketBalance} Tickets`;
yes.onRewardStateChange(callback)#yes.onRewardStateChange(callback: (state: RewardState) => void): () => void
Fires when ad readiness or the Ticket balance changes, so you redraw your offer instead of polling. Returns an unsubscribe function.
Returns
() => voidCall it to stop listening, e.g. when the offer closes.
Example
drawOffer(await yes.isRewardedAdReady());const off = yes.onRewardStateChange(state => drawOffer(state));// ...when the offer closesoff();
yes.reportRewardOffered(placement)#yes.reportRewardOffered(placement: string): void
Call it every time an offer appears on screen, whether or not the player takes it. No need to await. Without it the platform can't tell a placement nobody takes from one nobody sees.
Parameters
| Name | Type | Description |
|---|---|---|
placement | string | The Reward Placement name |
Example
showReviveOffer();yes.reportRewardOffered('revive');
yes.watchRewardedAd(placement)#yes.watchRewardedAd(placement: string): Promise<RewardedAdResult>
Play a rewarded ad straight away, with no platform screen. Your game is paused while it plays. Grant the reward only when rewarded is true. Watching an ad never gives the player Tickets.
Parameters
| Name | Type | Description |
|---|---|---|
placement | string | The Reward Placement name |
Returns
Promise<RewardedAdResult>{ rewarded: boolean }, plus errorCode ('NOT_LOADED' | 'USER_DISMISSED' | 'FAILED_TO_SHOW' | 'UNKNOWN') when not rewarded
Example
const result = await yes.watchRewardedAd('revive');if (result.rewarded) {revivePlayer();}
yes.spendTicket(amount, placement)#yes.spendTicket(amount: number, placement: string): Promise<TicketSpendResult>
Spend Tickets at the price you set. Each call is one spend; calling again charges again. If the player doesn't hold enough, the Yes Store opens with your game paused behind it, and the call resolves once it closes: spent if they bought enough, otherwise INSUFFICIENT_BALANCE.
Parameters
| Name | Type | Description |
|---|---|---|
amount | number | Tickets to spend, a whole number from 1 to 10 |
placement | string | The Reward Placement name |
Returns
Promise<TicketSpendResult>{ spent: boolean }, plus errorCode ('INSUFFICIENT_BALANCE' | 'FAILED') when not spent
Example
const result = await yes.spendTicket(3, 'booster');if (result.spent) {grantBooster();}
Don't hide a Ticket button on a short balance
spendTicket() handle the rest.Example: revive (ad or Tickets)
async function onGameOver() {const offer = showReviveOffer({ price: 1 }); // your own UIyes.reportRewardOffered('revive');const draw = ({ ready }) => offer.setAdEnabled(ready);draw(await yes.isRewardedAdReady());const off = yes.onRewardStateChange(draw);offer.onWatchAd(async () => {const { rewarded } = await yes.watchRewardedAd('revive');if (rewarded) { off(); offer.close(); revivePlayer(); }});offer.onPayTickets(async () => {const { spent } = await yes.spendTicket(1, 'revive');if (spent) { off(); offer.close(); revivePlayer(); }});offer.onDecline(() => { off(); offer.close(); showResults(); });}
Example: booster (Tickets only)
A placement doesn't have to offer an ad. Price it at what the reward is worth.
async function openBoosterOffer() {const offer = showBoosterOffer({ price: 3 }); // your own UIyes.reportRewardOffered('booster');offer.setBalance((await yes.isRewardedAdReady()).ticketBalance);const off = yes.onRewardStateChange(({ ticketBalance }) => offer.setBalance(ticketBalance));offer.onClose(off);offer.onBuy(async () => {const { spent } = await yes.spendTicket(3, 'booster');if (spent) grantBooster();});}
Interstitial Ads
You name the break, the platform decides
showInterstitialAd() at a natural break — a level end, a game over, a return to the menu. The platform decides whether an ad actually plays, and paces ads across every game the player plays, so don't add a cooldown or counter of your own. For opt-in rewarded ads, see Reward Placements.yes.showInterstitialAd(placement?)#yes.showInterstitialAd(placement?: string): Promise<InterstitialAdResult>
Tell the platform this is a good moment for an ad. If no ad fits or none is loaded, it resolves 'skipped' straight away. If an ad plays, your game is paused behind it and the call resolves when the ad closes. Either way, carry on the same. Only call it at a real break, never mid-action.
Parameters
| Name | Type | Description |
|---|---|---|
placement? | string | A label for your analytics, e.g. 'level_end'. At most 64 characters. |
Returns
Promise<InterstitialAdResult>{ status: 'shown' | 'skipped' }, plus a reason when skipped, for logging only. Don't change your game's behaviour based on it.
Example
async function onLevelComplete() {showResults();await yes.showInterstitialAd('level_end');showNextLevelButton(); // the same whether or not an ad played}
yes.isInterstitialAdReady()#yes.isInterstitialAdReady(): Promise<AdReadyStatus>
Whether an interstitial is loaded right now. Just a hint: you don't need to call it before showInterstitialAd(), which may still skip.
Returns
Promise<AdReadyStatus>{ ready: boolean }
Example
const { ready } = await yes.isInterstitialAdReady();
Games that never call showInterstitialAd() still earn: the platform may show an interstitial after a score submit. Once your game calls it, the platform leaves ad timing to your placements.
Haptic
yes.haptic()#yes.haptic(style?: HapticStyle): void
Trigger haptic feedback on the player's device. No need to await. Does nothing on devices without haptics.
Parameters
| Name | Type | Description |
|---|---|---|
style? | 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' | 'selection' | The haptic feedback style. Defaults to 'medium' |
Example
yes.haptic(); // default medium tapyes.haptic('success'); // positive feedbackyes.haptic('error'); // negative feedback
Language
Match the app's language
getLanguage() to read it and localize your game's UI so everything feels native to them.yes.getLanguage()#yes.getLanguage(): string
Returns the app's current language code. Synchronous, and already correct on your first line of game code — no need to wait for onReady().
Returns
stringOne of 'en', 'tr', 'es', 'pt-BR', 'de', 'fr', 'ar'. Defaults to 'en'. Note that 'pt-BR' is region-tagged and 'ar' is right-to-left.
Example
const lang = yes.getLanguage();const base = lang.split('-')[0]; // 'pt-BR' -> 'pt'setUILanguage(SUPPORTED[base] ? base : 'en');
yes.onLanguageChange(callback)#yes.onLanguageChange(callback: (language: string) => void): () => void
Fires when the player switches language while your game is running. Only on an actual change — read the initial value with getLanguage(). Returns an unsubscribe function.
Returns
() => voidCall it to stop listening.
Example
// Localize now, and again whenever the player switches.applyTranslations();yes.onLanguageChange(() => applyTranslations());
Never freeze the language at module scope
getLanguage() once into a module-level constant means your HUD and menus keep whatever language they had at startup, while text you render later comes out correct — so the game looks half-translated. Read it where you use it, or re-render from onLanguageChange().Saving Progress
Save your game's progress (levels, items, unlocks) with plain localStorage. On Yes it is backed up to the player's account and follows them to other devices, and it works offline. There is no extra API to call.
Each game gets up to 256KB per player. If a write goes over, your game keeps its local copy but the server refuses it, and you get a yes:storagerejected event on window.
localStorage.setItem('progress', JSON.stringify({ level: 12 }));window.addEventListener('yes:storagerejected', (e) => {// e.detail: { reason: 'cap_exceeded', capBytes, currentBytes }console.warn('Save is over the 256KB limit', e.detail);});
Testing Locally
Outside the Yes app, every SDK call resolves its “didn't happen” value at once: no player, no leaderboard, no ads, no Tickets. Your game should still run.
To see your game with real-looking data, load the mock host before the SDK. It stands in for the Yes app and ships in the @aspect_games/yes-sdk npm package as dist/yes-mock-host.js. Never upload it with your game.
<script src="yes-mock-host.js"></script><script src="yes-sdk.js"></script>
The mock host gives you a player named TestPlayer, a sample leaderboard, ads that always play, and 10 Tickets to spend (spends past that fail with INSUFFICIENT_BALANCE). To test pausing, run yesMockHost.command({ type: 'pause', gameId: 0 }) in the browser console.
Full Example
A complete game.js that uses every SDK feature:
let lives = 3;let score = 0;// Localize straight away, and again if the player switchesapplyTranslations(yes.getLanguage());yes.onLanguageChange(lang => applyTranslations(lang));// Greet the playeryes.onReady(async () => {const player = await yes.getPlayer();document.getElementById('playerName').textContent = player.name ?? 'Player';});// Called when the player completes a levelasync function onLevelComplete(levelScore) {score += levelScore;yes.submitScore(score); // no need to waityes.haptic('success');localStorage.setItem('score', String(score)); // saved to the player's accountshowResults();await yes.showInterstitialAd('level_end'); // may or may not play an adloadNextLevel();}// Show the leaderboard on the results screenasync function showBoard() {const board = await yes.getLeaderboard();if (board) drawLeaderboard(board.weekly);}// Reward Placement: an extra life for an ad or 1 Ticketfunction showExtraLifeOffer() {drawExtraLifeOffer(); // your own UIyes.reportRewardOffered('extra_life');}async function watchAdForLife() {const result = await yes.watchRewardedAd('extra_life');if (result.rewarded) lives++;}async function payTicketForLife() {const result = await yes.spendTicket(1, 'extra_life');if (result.spent) lives++;}
Best Practices
Check results, not errors
SDK calls never reject, so skip the try/catch. Grant rewards only when the result says so:
const ad = await yes.watchRewardedAd('revive');if (ad.rewarded) {giveReward();}
Score submission timing
Submit scores at natural game moments — end of level, game over, or achievement completion. Avoid spamming submissions during gameplay.
Ads only at real breaks
Call showInterstitialAd() between levels or on game over, never mid-action. Continue the same way whether or not an ad played.
Test with the mock host
Develop with the mock host loaded, then remove it before you upload.