Yes2SDK for Defold
A single SDK for your Defold HTML5 game. Integrate once against Yes2SDK, submit through the Yes2Games Dashboard, and the Yes2Games team handles the rest.
Supported platforms: Poki, CrazyGames, Yandex Games, GameDistribution, YouTube Playables and Jest.
Requirements
-
Yes2SDK runtime 2.10.0 or newer for referrals, subscriptions, notifications, context sharing, entry point data, the registration prompt, confirmed writes and
on_exit_requested. Older runtimes reportFEATURE_NOT_SUPPORTEDfor those calls, andon_exit_requestednever fires. -
Defold 1.10.2 or newer — the oldest version
build.defold.com, Defold's hosted extension build server, still compiles native extensions for. This SDK is a native extension, so on an older Defold the bundle fails at the build server withHTTP 501 — Engine version '<sha>' is not supported on the current server, whatever else your project does.Do not lower this number without first checking that the hosted server accepts the older SDK again. Defold prunes old SDKs from that server as new versions ship, so this floor moves up over time and never down. It is a floor of the hosted server rather than of the engine: Defold still publishes the older SDK archives, so a self-hosted extender may well build further back — untested here, and not something this SDK promises.
-
HTML5 build target (the extension is HTML5-only — non-HTML5 platforms get a warn-and-no-op stub)
Installation
Via Git Dependency (recommended)
In your game.project, add:
[project]
dependencies#0 = https://github.com/yes2games/yes2sdk-defold/archive/refs/tags/v1.9.0.zip
Then in Defold Editor: Project > Fetch Libraries.
Pinning the URL to a
tags/vX.Y.Zref keeps the resolved hash stable. Bump the tag when a newer release ships. Don't use/refs/heads/main.zip— branch archives are served less reliably by GitHub andFetch Librariesfails intermittently.
Channels
Two dependency URLs, and nothing else is a channel:
| Channel | Dependency URL | What it is |
|---|---|---|
| production | https://github.com/yes2games/yes2sdk-defold/archive/refs/tags/vX.Y.Z.zip | An immutable release tag at an exact commit. Use this in a shipping game. |
| integration | https://github.com/yes2games/yes2sdk-defold/archive/refs/tags/edge.zip | A mutable edge tag that moves to the newest commit on main that passed required CI. Use it to test against unreleased changes. |
# production — pin a release
dependencies#0 = https://github.com/yes2games/yes2sdk-defold/archive/refs/tags/vX.Y.Z.zip
# integration — track the tested tip of main
dependencies#0 = https://github.com/yes2games/yes2sdk-defold/archive/refs/tags/edge.zip
edge is a Git tag and nothing more: there is no edge GitHub Release, no
prerelease, and no edge-X.Y.Z. It moves, so the resolved hash changes under you
and a stale Defold dependency cache can serve older bytes — re-fetch libraries
after clearing .internal/lib if edge looks behind. Never ship a game against
it.
Via Local Copy
- Download the latest release from Releases
- Copy the
yes2sdk/folder into your project root:your-game/ ├── game.project ├── yes2sdk/ ← copy here │ ├── ext.manifest │ ├── include/ │ ├── src/ │ ├── lib/web/ │ └── yes2sdk.lua └── main/ - Close and reopen Defold Editor.
Quick Start
This is the minimum integration your game must have. The lifecycle has three distinct stages — don't chain them together:
App launch → yes2sdk.initialize (SDK ready)
Splash + loading → yes2sdk.set_loading_progress(0..100) as assets load
Game playable → yes2sdk.start_game (scene ready, accepting input)
local yes2sdk = require "yes2sdk.yes2sdk"
function init(self)
-- Stage 1 — at app launch, initialize the SDK.
yes2sdk.initialize(function(self, success, error)
if not success then
print("[Game] SDK init failed: " .. tostring(error))
return
end
print("[Game] SDK ready")
-- Stage 2 — load your assets and report progress (0..100).
-- The Defold engine template reports DOWNLOAD progress automatically.
-- Use set_loading_progress only for game-specific loading after the
-- engine starts (LiveUpdate, procedural generation, asset streaming).
-- Stage 3 — splash done, scene loaded, game is playable.
yes2sdk.start_game(function(self, success, error)
if success then
yes2sdk.session_gameplay_start()
end
end)
end)
end
function final(self)
yes2sdk.session_gameplay_stop()
end
Don't call
start_gamedirectly inside theinitializesuccess handler if you have a splash/loading phase. The platform's loading bar treatsstart_gameas "the game is playable now". For games with no post-engine loading phase, calling them back-to-back is fine (Defold's engine template handles the initial download progress for you).
Without this flow your game won't be accepted for review.
Core API
Implement everything in this section. Together these cover what Yes2Games needs to validate and monetize your game.
Lifecycle (required)
yes2sdk.initialize(function(self, success, error) ... end) -- once at startup
yes2sdk.set_loading_progress(progress) -- 0..100 as assets load
yes2sdk.start_game(function(self, success, error) ... end) -- when game is playable
Important: these three calls fire in three distinct stages.
initializeis at app launch.set_loading_progressis updated as your assets load.start_gameruns only when the game is actually playable (splash gone, scene loaded, accepting input).
Lifecycle events (required)
The platform tells the game when to pause, when it may resume and when the player muted or unmuted audio. Some platforms reject a game that ignores these. Subscribe inside the initialize success callback: the events are not available before the SDK is ready.
yes2sdk.initialize(function(self, success, error)
if not success then return end
-- Initial audio state, then follow every change.
set_game_audio(yes2sdk.session_is_audio_enabled())
yes2sdk.on_audio_enabled_change(function(self, enabled)
set_game_audio(enabled)
end)
yes2sdk.on_pause(function(self)
pause_game() -- stop the game loop, audio and network calls
end)
yes2sdk.on_resume(function(self)
resume_game() -- the game may continue
end)
-- The platform's account selection dialog (only some platforms show one).
yes2sdk.on_account_dialog_open(function(self) pause_game() end)
yes2sdk.on_account_dialog_close(function(self) resume_game() end)
end)
on_pause: the game must stop its loop, audio and network calls untilon_resume.on_resume: the game may continue. A resume is not guaranteed to follow every pause.on_exit_requested: on platforms that support it, the platform is about to close the game and the player has not confirmed leaving. Save synchronously inside the handler (for example withdata_set_string): the SDK flushes player data right after it returns, and async work started there is not awaited. Register it afterinitialize, like the other events.on_audio_enabled_change:enabledis a boolean. Keep the game's audio in line with it, and readsession_is_audio_enabled()once at startup for the initial state.on_account_dialog_open/on_account_dialog_close: pause while the dialog is open, resume when it closes. Platforms without such a dialog never fire them.- Each callback runs once per platform event. Registering again for the same event replaces the previous callback.
- Register from a long-lived script (see Callbacks and script lifetime).
Ads (required)
Interstitial ads run at natural break points. Rewarded ads run only when the player opts in.
Always wrap ad calls in
session_gameplay_stop()/session_gameplay_start(). Platforms count "active gameplay seconds" for monetization: leaving gameplay running during an ad inflates those numbers and is grounds for rejection.
function show_interstitial(self)
yes2sdk.session_gameplay_stop()
yes2sdk.ads_show_interstitial("level-end",
function(self) end, -- before_ad: pause
function(self) yes2sdk.session_gameplay_start() end, -- after_ad: resume
function(self) end -- no_fill: no ad, after_ad follows
)
end
function show_rewarded(self)
yes2sdk.session_gameplay_stop()
yes2sdk.ads_show_rewarded("extra-life",
function(self) end, -- before_ad: pause
function(self) yes2sdk.session_gameplay_start() end, -- after_ad: resume
function(self) end, -- ad_dismissed: no reward
function(self) grant_extra_life() end, -- ad_viewed: GRANT REWARD
function(self) end -- no_fill: no reward, after_ad follows
)
end
Rewarded ad firing order
The callbacks fire in this order. Pay attention: getting it wrong silently breaks reward logic:
before_ad → pause game (fires when the ad starts; usually skipped on no_fill)
(ad shown)
ad_viewed → grant reward (ONLY fires if the player watched the full ad)
or
ad_dismissed → no reward (fires if the player skipped/closed early)
or
no_fill → no ad available (fires if the platform couldn't deliver)
after_ad → resume game (always, fires after the result, no_fill included)
Every rewarded ad ends with exactly one of ad_viewed, ad_dismissed or no_fill, then after_ad, and after_ad fires at most once:
- If the platform reports a second result for the same ad, the SDK drops it and logs a warning. The first result wins.
- If the platform sends
after_adwithout a result, the SDK callsad_dismissedfirst (with a warning), thenafter_ad. It never callsad_viewedon its own, so a reward is only granted when the platform says the ad was watched. - An error raised inside one of your ad callbacks is logged as
[Yes2SDK] <name> callback error: ...and does not stop the next callback, so an error inad_dismissedstill letsafter_adresume the game. no_fillis always followed byafter_ad, for interstitials too: if the platform sends noafter_adby the next frame, the SDK calls it. Resume the game inafter_ad. The one exception is a call rejected because another ad is already in flight: it getsno_fillonly, since itsafter_adwould resume the game while the other ad is still on screen. Thatno_fillruns at once, inside theads_show_*call, and with no arguments, soselfisnilthere.- A watchdog covers ads that never report back. An ad that never starts gets
no_filland thenafter_adafter 30 s. An ad that stays on screen for 180 s getsafter_ad, preceded by a synthesizedad_dismissedif it is rewarded.
⚠️ Do NOT grant rewards in
after_ad.after_adfires for completion, dismissal, and no-fill alike, so granting rewards there gives them away on skip. Always grant inad_viewed.
Concurrent ad guard + readiness
yes2sdk.ads_is_ad_showing(): returnstruewhile aads_show_interstitialorads_show_rewardedis in flight (between the call andafter_ad/no_fill). Callingads_show_*again while one is already showing is rejected immediately andno_fillfires for the rejected call (noafter_adfollows it).ads_is_ad_showing()is alreadyfalseinsideno_fill, so you can retry from there, or on the next frame. Retry only from the ad's ownno_fill, and at most once: a rejected call'sno_filland the not-loaded path run synchronously insideads_show_*, so retrying without a bound recurses until the stack overflows. A retry from insideno_fillreceives the first ad'safter_adfirst, during theads_show_*call.yes2sdk.ads_is_rewarded_ad_available(): best-effort check whether a rewarded ad appears available right now. Most platforms don't expose explicit readiness, so this returnstruewhile the platform's ad module is loaded; the actualads_show_rewardedcall can still no-fill. Use it as a hint, not a guarantee.
local can_show_reward = not yes2sdk.ads_is_ad_showing()
and yes2sdk.ads_is_rewarded_ad_available()
gui.set_enabled(reward_button, can_show_reward)
Session / Gameplay (required)
Tells Yes2Games when an active round begins and ends. Also call session_gameplay_stop() before any ad and session_gameplay_start() after.
yes2sdk.session_gameplay_start()
yes2sdk.session_gameplay_stop()
local locale = yes2sdk.session_get_locale() -- e.g. "en", "ja", "ru"
Entry point data is whatever the player arrived with, for example from a shared link or after registering. It is always a table (empty when there is none, or on platforms that do not support it):
local entry = yes2sdk.session_get_entry_point_data()
if entry.invite then
show_welcome(entry.invite)
end
analytics_log_level_start/_endcan also trigger gameplay start/stop on some platforms. Calling both pairs is safe: the SDK keeps a single owner of the gameplay state, so a start or stop that is already in effect is not sent twice.
Data (required)
Key-value storage. Persists across sessions automatically.
yes2sdk.data_set_int("highScore", 9999)
yes2sdk.data_set_string("playerName", "Alice")
yes2sdk.data_set_float("volume", 0.75)
local score = yes2sdk.data_get_int("highScore", 0)
local name = yes2sdk.data_get_string("playerName", "Guest")
local volume = yes2sdk.data_get_float("volume", 1.0)
if yes2sdk.data_has_key("highScore") then
yes2sdk.data_delete_key("highScore")
end
data_set_string is fire and forget: it returns before the platform has stored anything. When a write must survive something that may end the session (for example right before showing a login prompt), use a confirmed write on platforms that support it:
yes2sdk.data_set_string_async("progress", "level-5", function(self, success, err)
-- success is false when the platform did not confirm the write; err is an error JSON
end)
yes2sdk.data_flush(function(self, success, err) end) -- write everything pending
yes2sdk.player_flush_data(function(self, success, err) end) -- write pending player_set_data values
data_set_string_async and data_flush work everywhere (on platforms with local storage they confirm right away). player_flush_data reports FEATURE_NOT_SUPPORTED where player_is_data_supported() is false. In the editor the mock confirms every call on the next frame without storing anything.
Analytics (recommended)
yes2sdk.analytics_log_level_start("level-1")
yes2sdk.analytics_log_level_end("level-1", 1500, true)
-- Time-based games (racing, time-attack) can include duration:
yes2sdk.analytics_log_level_end("level-1", 1500, true, 87.3)
yes2sdk.analytics_log_score(9999)
yes2sdk.analytics_log_tutorial_start()
yes2sdk.analytics_log_tutorial_end()
yes2sdk.analytics_log_game_choice("character", "wizard")
-- Custom event with optional params (params as a JSON string):
yes2sdk.analytics_log_event("boss_defeated")
yes2sdk.analytics_log_event("boss_defeated", json.encode({ level = 3, time = 42.5 }))
Custom events are delivered to Yandex Metrica as
reachGoal(event_name, params)on builds with a Metrica counter configured; on other platforms they are logged through the SDK's analytics pipeline.
Optional APIs
These modules add extra player-facing features. They are not guaranteed to be available at runtime — guard with a support check and handle the unsupported case gracefully. Don't make your core gameplay depend on them.
Support checks available: ads_is_interstitial_supported(), ads_is_rewarded_supported(), auth_is_supported(), player_is_data_supported(), player_is_bot_avatar_supported(), friends_is_supported(), banners_is_supported(), score_is_supported(), leaderboard_is_supported(), stats_is_supported(), config_is_supported(), review_is_supported(), iap_is_supported(), iap_is_subscription_supported(), referrals_is_supported(), notifications_is_supported(). (context_is_supported() exists too, but it can return false on platforms where sharing works, so do not use it as a gate: see Context sharing.)
if yes2sdk.ads_is_rewarded_supported() then
-- show a "Watch ad for reward" button
end
Auth
if yes2sdk.auth_is_authenticated() then
-- already signed in
else
yes2sdk.auth_sign_in(function(self, success, error)
if success then print("Signed in") end
end)
end
Registration prompt
On platforms that support it, auth_show_registration_prompt(options) opens the platform's minimal registration overlay and you draw the rest of the prompt yourself. It returns a handle, or nil and an error JSON string (read it with parse_error):
if not yes2sdk.auth_is_authenticated() then
-- save progress first (for example data_set_string_async, then data_flush)
local prompt, err = yes2sdk.auth_show_registration_prompt({
theme = "dark",
message = "Join me in the game! {{registrationCode}} is my code.",
data = { reward = "welcome_back" },
on_close = function(self) hide_my_prompt_ui() end,
})
if prompt then
-- wire your own buttons:
-- prompt.login() starts the platform login flow (the prompt stays open)
-- prompt.close() closes the prompt (on_close fires once)
else
print(yes2sdk.parse_error(err).code)
end
end
-
Guests only: a registered player gets
INVALID_OPERATION. Useauth_is_authenticated()to tell registered players apart. -
Save the player's progress before showing the prompt.
-
messageis optional. When given it must:- not be empty or whitespace only,
- be at most 140 characters, counting
{{registrationCode}}as written and an emoji as 2, - contain
{{registrationCode}}exactly once and no other{{...}}placeholder, - keep the code apart from neighbouring letters, digits or underscores with a space or punctuation.
Otherwise you get
INVALID_PARAM. Keep it short and plain: the platform may drop emoji and accented characters from a long pre-filled text. -
datacomes back fromsession_get_entry_point_data()after the player registers. -
on_closeruns once when the prompt closes, fromprompt.close()or the platform's own close button. It always runs after the call returns, never inside theprompt.close()orauth_show_registration_promptcall, and never whenauth_show_registration_promptreturned an error.login()andclose()returnfalseonce the prompt is closed. -
For a custom prompt on Jest, turn off Automatic login reminders in your game's Overview settings on the Yes2Games Dashboard (available once Jest is enabled for your studio). It is a per-game setting, not an SDK call.
-
Platforms without a registration prompt return
FEATURE_NOT_SUPPORTED.
Friends
if yes2sdk.friends_is_supported() then
yes2sdk.friends_list_friends(0, 10, function(self, success, page_json)
if success then
local page = json.decode(page_json)
for _, friend in ipairs(page.friends) do
print(friend.username, friend.id)
end
end
end)
end
Banners
if yes2sdk.banners_is_supported() then
yes2sdk.banners_show("sidebar-left", "300x250")
yes2sdk.banners_hide("sidebar-left")
yes2sdk.banners_hide_all()
end
Score
if yes2sdk.score_is_supported() then
yes2sdk.score_add(9999)
yes2sdk.score_submit("encrypted-score-string")
end
Player Data (cloud)
yes2sdk.player_get_data(json.encode({"level"}), function(self, success, data_json)
if success then
local data = json.decode(data_json)
print(data.level)
end
end)
yes2sdk.player_set_data(json.encode({level = 5}), function(self, success, error) end)
Bot avatars
Games that fill a lobby or leaderboard with computer-controlled players can ask the platform for a matching avatar. The username seeds the picture, so the same bot always gets the same avatar. Only some platforms generate them: gate the call on player_is_bot_avatar_supported() and keep your own art as the fallback.
if yes2sdk.player_is_bot_avatar_supported() then
yes2sdk.player_get_bot_avatar("RoboRita", "small", function(self, success, url)
if success then
-- load url as the bot's picture
else
-- use your own bot art
end
end)
end
sizeis"small","medium"(the default) or"large", and may be left out:player_get_bot_avatar("RoboRita", callback).- On success the callback gets the image URL as a plain string. An empty username or an unknown size fails with
INVALID_PARAM, and a platform without bot avatars fails withFEATURE_NOT_SUPPORTED(see Errors).
Game Extras
yes2sdk.game_happy_time() -- positive-moment signal
yes2sdk.game_copy_to_clipboard("https://...")
yes2sdk.game_invite_link(json.encode({roomId = "abc"}), function(self, success, url) end)
local settings_json = yes2sdk.game_get_settings()
game_happy_time()signals to the platform that the player just hit a positive moment — level cleared, achievement unlocked, boss defeated. Some platforms (notably CrazyGames) use this to time monetization prompts so they don't interrupt frustrating moments. Call it sparingly, only on genuine highs.
In-app purchases
Gate every purchase UI on iap_is_supported(). Product ids are the ones configured for your game on the platform.
-- Purchases waiting to be consumed. iap_consume_purchase takes one call at a
-- time, so they are consumed one by one: the next starts in the previous callback.
local consume_queue = {}
local consuming = false
local function consume_next()
local purchase = table.remove(consume_queue, 1)
consuming = purchase ~= nil
if not purchase then return end
yes2sdk.iap_consume_purchase(purchase.purchaseToken, function(self, success, error)
if not success then
-- Still unconsumed: iap_get_purchases returns it again on the next launch.
print("Consume failed: " .. yes2sdk.parse_error(error).code)
end
consume_next()
end)
end
local function finish_purchase(purchase)
-- Grants are keyed by purchaseToken and saved with the progress, so a purchase
-- whose consume failed is not granted a second time on the next launch.
if not save_data.granted[purchase.purchaseToken] then
grant_product(purchase.productId) -- give the item
save_data.granted[purchase.purchaseToken] = true
save_progress() -- persist it BEFORE consuming
end
table.insert(consume_queue, purchase)
if not consuming then consume_next() end
end
-- On launch, after initialize: finish purchases a previous session never completed.
if yes2sdk.iap_is_supported() then
yes2sdk.iap_get_purchases(function(self, success, purchases_json)
if success then
for _, purchase in ipairs(json.decode(purchases_json)) do
finish_purchase(purchase)
end
end
end)
end
-- Buying goes through the same queue.
yes2sdk.iap_purchase("coins_100", nil, function(self, success, result)
if success then
finish_purchase(json.decode(result))
elseif yes2sdk.parse_error(result).code ~= "IAP_PURCHASE_CANCELLED" then
show_purchase_failed()
end
end)
- Finish incomplete purchases on launch. A purchase can be paid for and then lost to a reload or a crash before the game granted it.
iap_get_purchasesreturns every purchase that was not consumed yet; grant each one that was not granted before, then consume them one at a time. - Grant and save before consuming. Consuming tells the platform the item was delivered. If the game consumes first and then fails to save, the player paid for nothing. Key saved grants by
purchaseToken: a purchase whose consume failed comes back on the next launch and must not be granted twice. - Purchase JSON fields:
purchaseToken(pass it toiap_consume_purchase),productId,paymentId,purchaseTime(ISO 8601),developerPayload(when you passed one), and, where the platform provides them,signedRequest(for server verification) andisSandbox(truewhen no real money changed hands; grant the item as usual but keep it out of revenue reporting). - Verify server side. For anything of value, send
signedRequestto your own server and check it there. Never trust a purchase on the client alone. - One checkout at a time. A second
iap_purchasewhile one is open is rejected: it logs a warning and its callback is never called. The same applies toiap_consume_purchase. Wait for the callback before the next call, as the queue above does. - The catalog:
iap_get_catalog(callback)returns a JSON array of products (productId,title,description,imageUri,price,priceCurrencyCode,priceAmount);iap_get_product(product_id, callback)returns one product, or the literal"null"when the id is unknown. - Failures carry an error code, see Errors. In the editor, purchases run against a mock:
mock_purchase_result = failingame.projecttests the failure path (see Editor Testing).
Subscriptions
On platforms that support it, players can subscribe to a product. Gate subscription UI on iap_is_subscription_supported().
local PREMIUM = "premium_monthly"
if yes2sdk.iap_is_subscription_supported() then
yes2sdk.iap_get_subscriptions(function(self, success, subscriptions_json)
if not success then return end
for _, sub in ipairs(json.decode(subscriptions_json)) do
if sub.productId == PREMIUM then
if sub.isActive then
grant_premium() -- already subscribed: never offer it again
else
show_subscribe_button(sub) -- sub.price, sub.billingPeriod, sub.trialEligible
end
end
end
end)
end
-- When the player taps the button:
yes2sdk.iap_subscribe(PREMIUM, function(self, success, result_json)
if success then
local result = json.decode(result_json)
if result.status == "subscribed" then
grant_premium()
end -- "cancelled": the player closed the checkout
elseif yes2sdk.parse_error(result_json).code ~= "IAP_PURCHASE_CANCELLED" then
show_purchase_failed()
end
end)
- Never re-offer a subscription the player already holds. Check
isActivefromiap_get_subscriptions, oriap_get_subscription_status(product_id, callback), before showing a subscribe button. iap_subscribe(product_id, callback)returns{"status":"subscribed","subscription":{...}}, or{"status":"cancelled"}when the player closed the checkout. A closed checkout can also arrive as a failure with codeIAP_PURCHASE_CANCELLED(read it withparse_error): treat it as a change of mind, not an error. One subscribe at a time: a second call while one is open fails on the next frame withINVALID_OPERATION.- Subscription JSON fields:
productId,title,description,price,priceAmount,priceCurrencyCode,billingPeriod(weekly,monthlyoryearly),isActive(grant the entitlement when true),trialEligible,introOfferandretentionOffer({priceAmount, durationPeriods}ornull), and where the platform provides themisSandboxandsignedRequest. VerifysignedRequeston your own server, as for purchases. iap_get_subscription_status(product_id, callback)returns{"isActive":true,"productId":"...","expiresAt":...,"willRenew":true}(expiresAtin Unix milliseconds;expiresAtandwillRenewonly when known).iap_cancel_subscription(product_id, callback)calls back with(self, success, cancelled):cancelledis a boolean on success, the error JSON on failure.iap_claim_retention_offer(product_id, callback)claims the subscription's retention offer and returns the updated subscription JSON.- In the editor, the mock offers
yes2.mock.premium.monthlyand keeps subscription state for the session;mock_subscribe_resultselects the checkout outcome (see Editor Testing).
Context sharing (image)
Share a message with an optional image through the platform's share flow, on platforms that support it.
yes2sdk.context_share({
intent = "SHARE", -- "SHARE" (default), "INVITE", "REQUEST" or "CHALLENGE"
image = data_url, -- see below
text = "Look at my score!",
data = { score = 120 }, -- optional, handed back to whoever opens the share on platforms that support it
}, function(self, success, err)
if not success then
print("Share failed: " .. yes2sdk.parse_error(err).code)
end
end)
imageis a URL on most platforms. Some platforms require a base64 PNG or adata:image/png;base64,...URL, so prefer the data URL. Fields a platform does not use (image,textordata) are ignored.- Do not gate
context_shareoncontext_is_supported(): it can return false on platforms where sharing works. Callcontext_shareand handle the failure; aFEATURE_NOT_SUPPORTEDcode means the platform has no share. - The callback is optional:
yes2sdk.context_share(options)is a fire-and-forget share. - Passing options that are not a table or a JSON string fails the callback with
INVALID_PARAM.
Leaderboard
if yes2sdk.leaderboard_is_supported() then
yes2sdk.leaderboard_set_score("weekly", 1500, nil, function(self, success, entry_json)
if success then
local entry = json.decode(entry_json)
print("Rank " .. entry.rank .. ", score " .. entry.formattedScore)
end
end)
-- Top 10 entries (count, offset).
yes2sdk.leaderboard_get_entries("weekly", 10, 0, function(self, success, entries_json)
if success then
for _, entry in ipairs(json.decode(entries_json)) do
print(entry.rank, entry.playerName, entry.score)
end
end
end)
end
leaderboard_get(name, callback) returns the leaderboard (name, contextId, entries). leaderboard_get_player_entry(name, callback) returns the player's own entry, or the literal "null" when the player is not ranked. The third argument of leaderboard_set_score is an optional metadata string (pass nil to omit).
Stats
if yes2sdk.stats_is_supported() then
yes2sdk.stats_increment(json.encode({ kills = 1 }), function(self, success, stats_json)
if success then
print("Kills: " .. json.decode(stats_json).kills)
end
end)
yes2sdk.stats_get(json.encode({ "kills", "deaths" }), function(self, success, stats_json) end)
yes2sdk.stats_set(json.encode({ deaths = 0 }), function(self, success, error) end)
end
Remote config
if yes2sdk.config_is_supported() then
local options = json.encode({ defaults = { new_shop = "false" } })
yes2sdk.config_get_flags(options, function(self, success, flags_json)
if success then
local flags = json.decode(flags_json)
enable_new_shop(flags.new_shop == "true")
end
end)
end
Flag values are strings. Pass "{}" when you have no options; options that are not valid JSON are treated as no options.
Review
Ask for a rating at a positive moment, never in the middle of play.
if yes2sdk.review_is_supported() then
yes2sdk.review_can_review(function(self, success, eligibility_json)
if success and json.decode(eligibility_json).canReview then
yes2sdk.review_request_review(function(self, success, result_json) end)
end
end)
end
Notifications
Schedule a reminder that is shown to the player later, on platforms that support it. Gate it on notifications_is_supported(). On some platforms the player must be registered before notifications are delivered.
if yes2sdk.notifications_is_supported() then
-- One notification per day for the next week. The id is per day, so
-- scheduling the sequence again replaces it instead of piling up copies.
for day = 1, 7 do
yes2sdk.notifications_schedule({
id = "daily_" .. day,
title = "Your reward is ready",
body = "Come back for the day " .. day .. " bonus.",
scheduled_in_days = day,
image_asset_id = "reward_day_" .. day,
cta_text = "Play",
priority = "medium",
data = { day = day },
}, function(self, success, result_json)
if not success then
print(yes2sdk.parse_error(result_json).message)
end
end)
end
end
yes2sdk.notifications_cancel("daily_3", function(self, success, err) end)
yes2sdk.notifications_cancel_all(function(self, success, err) end)
- Options:
id,title(required, a string; it may be empty, and platforms that allow it then send the notification without a title),body,scheduled_in_days(a whole number from 0 to 7) ordelay_seconds(use one of them, not both),cta_text,priority(low,medium,highorcritical),image_asset_idorimage_data_url(use one of them),icon_url,data. A JSON string is accepted too and is passed through as is, so use the camelCase names in that case. - Same id replaces. Scheduling with an id that is already scheduled replaces the earlier notification. Without an id one is generated and returned.
- Result: the callback gets
{"id","title","body","scheduledAt"}withscheduledAtin milliseconds since the epoch. Invalid options fail withINVALID_PARAM.
Referrals
On platforms that support it, a player can invite friends with a referral link and the game can list who joined. Gate it on referrals_is_supported().
if yes2sdk.referrals_is_supported() then
-- reference is a stable campaign key (required); data reaches the invited player
yes2sdk.referrals_share({ reference = "party_mode_v1", data = { from_level = 3 }, title = "Play with me" },
function(self, success, result)
if success and not json.decode(result).canceled then
-- the invite flow was completed
end
end)
yes2sdk.referrals_list(function(self, success, result)
if success then
local list = json.decode(result)
-- list.referrals["party_mode_v1"] = { { playerId = "...", joinedAt = "..." }, ... }
-- Send list.signedRequest to your server and verify it there before granting any reward.
end
end)
end
referrals_share options: reference (required, a non-empty string), data (table), title, text and image (base64 data URL, PNG, JPEG or WebP, at most 2 MB). A missing or empty reference fails the callback with INVALID_PARAM.
Two more options are used only on platforms that support them (Jest today):
onboarding_slug: the slug of a game that invited players go through first, for example a tutorial game.notification_templates: notifications sent to the referrer as invited players join. Each template is{ min_conversion_count = <whole number, 0 or more>, variants = { ... } }with at least one variant{ title, body, cta_text, image_reference }(bodyandcta_textare required;image_referenceis the id of a pre-approved image). The platform uses the template with the highestmin_conversion_countthe referrer has reached and picks one of its variants.
yes2sdk.referrals_share({
reference = "party_mode_v1",
onboarding_slug = "party-tutorial",
notification_templates = {
{ min_conversion_count = 1, variants = { { body = "A friend joined your party!", cta_text = "Play" } } },
{ min_conversion_count = 5, variants = { { title = "Party Mode unlocked", body = "Five friends joined.", cta_text = "Celebrate" } } },
},
}, function(self, success, result) end)
The snake_case names above are mapped for you. In a JSON string use the camelCase names (onboardingSlug, notificationTemplates, minConversionCount, ctaText, imageReference). Malformed values fail with INVALID_PARAM. Both calls report failures through the usual error JSON (see Errors).
Callbacks and script lifetime
- Every async function except the
ads_show_*calls takes a callbackfunction(self, success, result). Overlapping calls to the same function each get their own callback, with their own result. The exceptions:iap_purchase,iap_consume_purchaseand theads_show_*calls reject a second call while one is open,iap_subscribefails a second call while one is open withINVALID_OPERATION, andinitializeandstart_gameare called once per session. - A call that fails at runtime is reported through its callback with
success == falseand an error string, not raised into your script; only wrong argument types raise. That includes a call the loaded SDK does not provide (see Errors). - Call SDK functions from a long-lived script, for example the script of your main collection. The callback and the SDK's own timers belong to the script instance that made the call. A script in a collection proxy that gets unloaded, or that is paused with a time step of 0 while an ad is up, can miss its callbacks or delay the ad's release.
- If the script instance that made a call is deleted before the response arrives, the response is dropped and a warning is logged. Nothing runs against the deleted instance.
Errors
Behaviour change: failure strings used to be free text or a raw JSON dump of whatever the platform rejected with. Every failed callback now receives one JSON shape with a stable
code. Code that compared or printed the old text should switch toparse_error.
When a callback reports success == false, its error argument is a JSON string with exactly three string fields:
{"code":"IAP_PURCHASE_CANCELLED","message":"The player closed the checkout","context":"iap.purchaseAsync"}
code: a stable identifier to branch on. It is the SDK's own code when the failure carries one, otherwise one of the fallbacks below.message: human readable text for logs. Do not match on it, it can change.context: the call that failed.
Fallback codes added by the extension itself:
| Code | Meaning |
|---|---|
NOT_INITIALIZED | The SDK runtime (or the module behind the call) is not available, or the SDK is not initialized yet. |
FEATURE_NOT_SUPPORTED | The loaded SDK does not provide this call, for example an older runtime without the method. |
INVALID_PARAM | The arguments were rejected before the call was made, e.g. a JSON string that does not parse. |
UNKNOWN_ERROR | Anything else. Check message. |
Common SDK codes games may branch on:
| Code | Meaning |
|---|---|
IAP_PURCHASE_CANCELLED | The player closed the checkout without paying. Not an error to retry or report. |
IAP_PURCHASE_FAILED | The purchase did not go through. |
IAP_ALREADY_PURCHASED | The player already owns this product. |
IAP_NOT_AVAILABLE | Purchases are not available right now. |
PLAYER_NOT_AUTHENTICATED | The call needs a signed in player. |
FEATURE_NOT_SUPPORTED, PLATFORM_NOT_SUPPORTED | Not available on this platform. |
NETWORK_FAILURE, TIMEOUT | Transient, usually worth a later retry. |
INVALID_PARAM | An argument was rejected. |
INVALID_OPERATION | The call is not allowed right now, for example a second iap_subscribe while one is open, or a registration prompt for a player who is already registered. |
PLATFORM_ERROR | The platform reported a failure. |
yes2sdk.parse_error(err) turns the error into a table { code = string, message = string, context = string }. It never raises: a plain string that is not this JSON (for example from an older SDK) comes back as code = "UNKNOWN_ERROR" with the string as message, and nil gives an empty message.
yes2sdk.iap_purchase("coins_100", nil, function(self, success, result)
if success then
local purchase = json.decode(result)
-- grant, then consume
else
local err = yes2sdk.parse_error(result)
if err.code == "IAP_PURCHASE_CANCELLED" then
-- the player changed their mind, nothing to report
else
print("Purchase failed: " .. err.code .. " (" .. err.message .. ")")
end
end
end)
Platform notes
Jest
Yes2SDK loads the platform SDK itself. Do not add Jest's own SDK script or engine package to your project. Platform guide: https://developer.yes2games.com/docs (the Jest guide is being published there).
Supported on Jest:
- Player, including Bot avatars, and Auth with the Registration prompt (guests only)
- Data (1 MB) and Lifecycle events, including
on_exit_requested - In-app purchases and Subscriptions
- Notifications (
scheduled_in_days0 to 7, with images) - Session entry point data (
session_get_entry_point_data) - Referrals (with
onboarding_slugandnotification_templates) and Context sharing (image) - Analytics (logging only)
Not supported: banners, leaderboard, stats, review, remote config, friends and score. Jest has no in-game ads: ads_is_interstitial_supported(), ads_is_rewarded_supported() and ads_is_rewarded_ad_available() return false, and ads_show_interstitial / ads_show_rewarded call no_fill and then after_ad (before_ad, ad_viewed and ad_dismissed never fire). Never rely on ads, and grant no reward from them.
Launch checklist:
- Save as a guest before any login prompt.
- Schedule day 1 to day 7 notifications with images for registered players.
- Grant and consume incomplete purchases from
iap_get_purchasesat startup, and never offer a subscription the player already holds. VerifysignedRequeston your server before granting value. - Show a registration prompt to guests. If you use your own, turn off Automatic login reminders in the game's Overview settings on the Yes2Games Dashboard.
- Save inside
on_exit_requested, synchronously. - Use Jest's Auto loading screen mode for now. Still call
set_loading_progressas assets load andstart_gamewhen the game is playable. In Manual mode Jest closes the game after 15 seconds without a progress update, and Yes2SDK only keeps it alive once it has initialized, so a long engine download can hit that timeout. - Use relative asset paths, and read
session_get_entry_point_datainstead of URL parameters.
Integration Checklist
Your build is ready for review when:
-
initializeis called at startup -
set_loading_progressis called as assets load (or you rely on the engine template's automatic download progress) -
start_gameis called when the game is playable - Interstitial ads run at natural break points
- Rewarded ads grant reward only in
ad_viewed -
session_gameplay_stop()is called before every ad;session_gameplay_start()after - Gameplay resumes in
after_ad(no_fillis followed byafter_ad) -
data_*functions are used for persistent player data - Lifecycle events are handled:
on_pause/on_resumepause and resume the game,on_audio_enabled_changefollows the platform's mute state - If the game sells items: incomplete purchases from
iap_get_purchasesare granted and consumed on launch
The QA Inspector in the Yes2Games Dashboard validates all of this automatically.
Branded Loading Screen (Optional)
Yes2SDK includes a custom HTML5 template with an animated Yes2Games loading screen. To use it, add this to your game.project:
[html5]
custom_html_shell = /yes2sdk/html5/engine_template.html
This replaces the default Defold loading bar with:
- Animated Yes2Games logo with breathing glow effect
- Slim progress bar with shimmer animation
- Smooth fade-out when loading completes
The template automatically reports Defold's download progress to the loading screen — no Lua code needed for the initial load. set_loading_progress(progress) is for game-specific loading that happens after the engine starts (LiveUpdate, procedural generation, etc.).
Note: Calling
set_loading_progress()afterstart_game()has no visible effect — the loading screen is already dismissed by then.
Build & Submit
- Build: Project > Bundle > HTML5 Application > Create Bundle
- Zip the output folder (the folder containing
index.html) - Upload the zip to the Yes2Games Dashboard
- The dashboard handles SDK injection, platform bundling, and walks you through the QA Inspector and review request.
Always use Project > Bundle for HTML5, not Project > Build. The regular Build command doesn't compile native extensions through the build cloud.
Editor Testing
The native extension is HTML5-only. In the Defold editor, yes2sdk.* calls run against a functional mock so you can test your integration without bundling:
initialize/start_gamesucceed on the next frame- Ads play a timed mock flow (3s interstitial, 5s rewarded) and then fire the full callback sequence, so pause-resume wiring in
before_ad/after_adand the reward path inad_viewedare exercised like a real ad - Referrals work too:
referrals_is_supported()returns true,referrals_sharesucceeds andreferrals_listreturns an empty list - Bot avatars:
player_is_bot_avatar_supported()returns true andplayer_get_bot_avatarsucceeds on the next frame with a placeholdermock://bot-avatar/<size>/<username>URL. It is not a loadable image, so your fallback art path runs context_sharesucceeds on the next frame and prints the share, andcontext_is_supported()returns true (real platforms may report false even where sharing works, so do not gate on it)- IAP works end to end:
iap_is_supported()returns true,iap_get_catalogreturns a sample catalog,iap_purchaseaccepts any product id and resolves with a realistic purchase payload, andiap_get_purchases/iap_consume_purchaseoperate on a session purchase list. Mock purchases carry"isSandbox":true. Subscriptions are mocked too: a sampleyes2.mock.premium.monthlyoffer, subscribe / cancel / retention offer / status on session state auth_show_registration_promptreturns a handle:login()prints a line,close()fireson_closeon the next framenotifications_is_supported()returns true,notifications_scheduleechoes the notification with a computedscheduledAt, andnotifications_cancel/notifications_cancel_allsucceed- Confirmed writes:
data_set_string_async,data_flushandplayer_flush_datasucceed on the next frame without storing anything - Everything else keeps the one-time-warning stub with sensible defaults
Configure the mock in game.project (all keys optional):
[yes2sdk]
mock = 0
mock_rewarded_result = dismissed
mock_ad_result = nofill
mock_purchase_result = fail
mock_subscribe_result = cancelled
mock_entry_point_data = {"invite":"friend1"}
mock_referral_result = canceled
mock = 0disables the mock entirely (old stub behavior). Default: enabled.mock_rewarded_result = dismissedmakes rewarded ads firead_dismissed(no-reward path). Default:viewed.mock_ad_result = nofillmakes ad calls fail withno_fill. Default:normal.mock_entry_point_datais a JSON object string returned bysession_get_entry_point_data(). Default:{}.mock_purchase_result = failmakesiap_purchasefail with anIAP_PURCHASE_FAILEDerror (see Errors). Default:success.mock_referral_result = canceledmakesreferrals_sharereport{"canceled":true}. Default:shared(reports{"canceled":false}).mock_subscribe_result = cancelledmakesiap_subscribereport{"status":"cancelled"};failmakes it fail with anIAP_PURCHASE_FAILEDerror. Default:subscribed.
The mock is editor/desktop only. HTML5 bundles always use the real platform SDK, and a missing extension in an HTML5 build still prints the loud bundling warning. For richer simulation (specific locales, network conditions), use the QA Inspector in the Yes2Games Dashboard.
Running alongside other SDKs
Real games often ship with multiple platform SDKs in the same build (Yes2SDK + Poki + Yandex + Playgama, etc.). A few ground rules to keep them from stepping on each other:
- Init order. Initialize Yes2SDK first. Yes2SDK figures out which actual platform is hosting the game and routes through it — initializing your own platform SDK directly first can race with Yes2SDK's detection.
- One owner for pause / resume. Pick one SDK to drive your game's pause state. If both Yes2SDK and another SDK call resume/pause via callbacks, you'll get oscillation. Recommended: handle pause/resume only via Yes2SDK's
ads_show_*before_ad/after_adcallbacks. - One owner for ads. Don't call ads via two SDKs in the same session — the platform almost always rejects the second call. Pick the SDK that targets the platform you're actually hosted on.
Troubleshooting
yes2sdk is nil at runtime
The native extension wasn't compiled. You used Project > Build instead of Project > Bundle > HTML5 Application > Create Bundle. Bundle compiles extensions through Defold's build cloud.
"Couldn't install dependencies" when fetching the library
Use a tagged release URL, not a branch archive. GitHub serves tagged archives more reliably:
# Good — tagged release
dependencies#0 = https://github.com/yes2games/yes2sdk-defold/archive/refs/tags/v1.9.0.zip
# Bad — branch archive (intermittent failures)
dependencies#0 = https://github.com/yes2games/yes2sdk-defold/archive/refs/heads/main.zip
If it still fails, fall back to the local-copy install (see Installation > Via Local Copy).
excluded_content.zip 404 in console
This is Defold's LiveUpdate feature looking for excluded content. If your project does not use Exclude Resources (the default), this 404 is harmless.
If your project does use Exclude Resources (game.project → liveupdate.enabled), this 404 means the excluded content was not uploaded alongside your game bundle. Include excluded_content.zip in your upload, or disable Exclude Resources and re-bundle.
Game plays but no SDK events in the QA Inspector
- Check the browser console for
[Yes2SDK]log messages. - If no logs appear, the extension wasn't included — see "extension nil" above.
- Make sure
yes2sdk.initialize()is called early ininit()of your main script.
License
MIT — see LICENSE.