API Docs

GameSDK core reference

AdminUpdated Sep 22, 2026

GameSDK core reference

GameSDK is the small JavaScript library your game uses to talk to the Cool GPT Games page it is running in. This page covers the core: loading the SDK, the window.GameSDK object, the lifecycle calls (ready, start, gameOver), getPlayer, the event system, and the conventions every SDK method follows.

Feature-specific methods have their own pages:

Area

Methods

Page

Saves

saveData, loadData, listSaves, deleteSave

Saves & cloud progress

Scores

submitScore, getMyScore, replay.*

Scores, leaderboards & anti-cheat

Progression & social

awardXp, unlockAchievement, social.*

XP, achievements & social

Ads

requestAd

Ads & rewards

Store

economy.*

In-game economy

Tournaments

tournaments.*

Tournaments

Live-ops

getConfig, trackEvent, reportError

Remote config & analytics

Multiplayer

connectRoom, listRooms

Multiplayer

Ranked

ranked.*

Ranked matchmaking

For the bigger picture (sandbox, origins, play sessions), see How games run.


Loading the SDK

Add one script tag to your index.html, before any script that uses it:

<script src="/sdk/game-sdk.js"></script>
<script src="game.js"></script>
  • Use the relative path /sdk/game-sdk.js exactly as written. Your game is served from the game CDN, so this path resolves to the SDK there.

  • The same file is available at https://cdn.coolgptgames.com/sdk/game-sdk.js. Referencing that absolute URL also passes the upload scanner (it's the same origin your game is served from), but the relative path is recommended.

  • There is nothing to install and no npm package for in-game use. The file is a plain script (not a module) and it creates window.GameSDK as soon as it runs.

  • Don't add async to the SDK tag unless your own code waits for it. With a normal tag, window.GameSDK exists by the time the next script runs.

Always guard for the SDK being missing, because it won't load when you open the game locally (see Running outside the site):

var SDK = window.GameSDK || null;
if (SDK) SDK.ready();

Or define a stub with the methods you use, so the rest of your code doesn't need if checks:

var SDK = window.GameSDK || {
  ready: function () {},
  start: function () {},
  gameOver: function () {},
  submitScore: function () {},
  saveData: function () { return Promise.resolve({ ok: false, error: "no_sdk" }); },
  loadData: function () { return Promise.resolve(null); },
  getPlayer: function () { return Promise.resolve({ signedIn: false, level: 1, handle: null }); },
  on: function () { return this; }
};

Runtimes

The SDK is a plain script, so it works in any runtime where you control the HTML and can run JavaScript.

Runtime

How the SDK gets in

HTML5

You add the <script src="/sdk/game-sdk.js"> tag yourself.

PICO-8, Twine, Bitsy

The site does not add it for you. Your exported HTML is served as-is. To use the SDK, add the same script tag to the exported HTML before zipping, and call GameSDK from JavaScript your export can run.


window.GameSDK

After the script loads, window.GameSDK has these members:

Member

Kind

Documented on

ready()

lifecycle

this page

start()

lifecycle

this page

gameOver(score?)

lifecycle

this page

getPlayer()

player

this page

on(event, callback)

events

this page

submitScore, getMyScore, replay

scores

Scores, leaderboards & anti-cheat

saveData, loadData, listSaves, deleteSave

saves

Saves & cloud progress

awardXp, unlockAchievement, social

progression

XP, achievements & social

requestAd

ads

Ads & rewards

economy

store

In-game economy

tournaments

tournaments

Tournaments

getConfig, trackEvent, reportError

live-ops

Remote config & analytics

connectRoom, listRooms

multiplayer

Multiplayer

ranked

ranked 1v1

Ranked matchmaking


Conventions

These apply to every SDK method.

Fire-and-forget vs. promises

  • Methods that only tell the page something (ready, start, gameOver, submitScore, deleteSave, trackEvent, reportError, social.viewProfile) return undefined. You get no confirmation that they worked.

  • Methods that ask for something return a Promise. That includes saveData, which resolves with whether the save landed (you can still ignore it).

  • SDK promises never reject, and they always settle. If something fails, a promise resolves with a fallback value (null, [], {}, or an object with an error or reason field). Every request has a built-in timeout (see the table below), so no SDK promise is left pending. Don't rely on .catch() for SDK errors. Check the resolved value instead.

Timeouts

Every promise-returning method has a built-in timeout. If the page doesn't answer in time, the promise resolves with a fallback value:

Method

Built-in timeout

Value on timeout

getPlayer()

5 s

Last known player info. Before any answer, that is { signedIn: false, level: 1, handle: null }.

loadData(key)

8 s

null

saveData(key, value)

8 s

{ ok: false, error: "timeout" }

awardXp(...)

8 s

{ granted: 0, total: 0, level: <last known level>, leveledUp: false }

unlockAchievement(key)

8 s

{ key, name: undefined, alreadyHad: false, unlocked: false, reason: "timeout" }

getMyScore()

6 s

{ score: null, rank: null }

listSaves()

6 s

[]

getConfig()

6 s

{} (not cached, so the next call retries)

social.getFriends(), social.getLeaderboard()

6 s

[]

social.addFriend(handle)

6 s

{ handle, status: "error" }

tournaments.*, economy.*

6 s

null

ranked.*

12 s

null

replay.ready()

6 s

A local seed string "local.<timestamp>"

requestAd(...)

60 s (the player may be watching the ad)

{ error: "timeout" }, and the adError event fires with { reason: "timeout" }

A late answer that arrives after the timeout is ignored for that call (for getPlayer() it still fires the player event).

listRooms() is a direct network request rather than a message to the page. It resolves null if the request fails. See Multiplayer.

Message rate cap

The page accepts at most 30 SDK messages per second from your game. Messages over the cap are silently dropped. A dropped request never gets an answer, so its promise resolves with the timeout value above, seconds later. Don't call SDK methods every frame. Batch analytics and save on meaningful events, not continuously.

Many SDK calls also become requests from the player's browser to the Cool GPT Games API, which applies its own per-IP limit (100 requests per minute across the whole API) plus per-feature limits. See Errors & limits.

Parallel calls are safe

Every request carries its own request id, and the page echoes it on the answer, so each promise resolves with the answer to its call. You can call any method again (including loadData for the same key) before an earlier call has answered. A few methods treat parallel calls specially:

Method

Parallel calls

getPlayer()

Share one lookup on the page.

getConfig()

Share one request, and resolve with the same object.

requestAd()

Only one ad runs at a time. A second call while an ad is running resolves { error: "busy" } straight away.

Arguments are coerced, not validated

The SDK converts arguments with String(...) / Number(...) where the method needs it, and doesn't throw on bad input. Invalid values are dropped silently further down the line (by the page or the API). Pass the documented types.

No unsubscribe

on() has no matching off(). Register each listener once, at startup.

Running outside the site

The SDK only works when your game runs inside the Cool GPT Games page (on the game's page, or in the site's embed player). It sends every request to the site's origin, https://coolgptgames.com, and ignores messages from any other origin.

Situation

What happens

You open index.html from disk, or serve the folder from a local server

/sdk/game-sdk.js doesn't resolve, the script fails to load, and window.GameSDK is undefined. Use a guard or stub.

The real SDK loads, but the page around it isn't Cool GPT Games (for example, the Preview build button in your dashboard opens the game in its own tab, or someone frames the bundle elsewhere)

Every message is silently dropped. Fire-and-forget calls do nothing. Promises resolve with the timeout values above once their timeouts pass.


ready()

GameSDK.ready(): void

Tells the page your game has loaded and is showing its first screen.

Parameters: none.

Returns: undefined.

What it does:

  • Hides the site's loading overlay (the spinner over your cover image) immediately.

  • In the embed player, tells the host page ready (see Embedding & the score bridge).

If you never call it: the overlay still disappears about 1.2 seconds after your index.html finishes loading. If the frame never finishes loading within about 15 seconds (plus a short runtime-specific warm-up), the page shows "This game failed to load".

Guests / signed-in: same for both. No sign-in needed.

Runtimes: all, as long as the SDK is included.

Limits: none beyond the 30 messages/second cap.

Failure behaviour: silently ignored outside the site.

// After your first frame is drawn and assets needed for the title screen are loaded:
drawTitleScreen();
if (window.GameSDK) GameSDK.ready();

Pitfalls:

  • Call it once your first screen is actually visible. Calling it before loading large assets reveals a blank frame.

  • Calling it more than once is harmless, but each call sends another ready to an embedding host page.


start()

GameSDK.start(): void

Tells the page that gameplay has begun. This is what starts the play session, and the play session is how plays, play time, XP eligibility, leaderboard eligibility, mid-roll ads and analytics are all measured. See How games run for the full model.

Parameters: none.

Returns: undefined.

What it does (on the first call for this page load):

  1. Asks the site to start a play session for your game. This works for guests too.

  2. Starts a heartbeat every 15 seconds that measures play time.

  3. Sends your game its server-issued replay seed (fires the replaySeed event). Only matters if you use verified replays. See Scores, leaderboards & anti-cheat.

  4. In the embed player, tells the host page gamestart.

Later calls on the same page load reuse the same session. They don't start a new one. Calling start() at the beginning of every round is fine and common.

If you never call it (and never call replay.ready(), which also starts a session), there is no play session, so:

Feature

Without a session

Plays / play time

Not counted

awardXp

Resolves with granted: 0

unlockAchievement

Nothing is unlocked. Resolves unlocked: false, reason: "no_session"

submitScore

Dropped

Mid-roll ads (requestAd())

Resolve { error: "no_session" } (and adError fires)

trackEvent / reportError

Dropped

Rewarded ads

Still work (they don't need a session)

Guests / signed-in: both get a session. Features that record progress (XP, achievements, scores) additionally need a signed-in player.

Limits:

  • Sessions can only be started for published games.

  • Starting sessions is limited to 60 per hour per IP address. When the limit is hit, no session is created and nothing tells your game.

  • Each browser gets at most 20 counted sessions per game per 24 hours. Sessions after that still start, but they're flagged and don't count as plays or earn XP or scores.

Failure behaviour: silent. Your game isn't told whether a session started.

Runtimes: all, as long as the SDK is included.

function beginRound() {
  score = 0;
  state = "playing";
  if (window.GameSDK) GameSDK.start();
}

Pitfalls:

  • Session start is asynchronous. Calls made in the same moment as start() (for example, an immediate submitScore) can arrive before the session exists and be dropped. In practice, XP needs at least 30 seconds of session time and scores need about a minute (see How games run), so award those later in play.

  • Calling start() on page load for a game with a long menu makes play time include menu time. That's allowed, but call it when play actually begins if you want accurate numbers.


gameOver(score?)

GameSDK.gameOver(score?: number): void

Tells the page a run has ended.

Parameters:

Name

Type

Required

Constraints

score

number

no

Must be a real JavaScript number to be passed on. A string like "12" is sent by the SDK but ignored by the page.

Returns: undefined.

What it does: in the embed player, tells the host page gameover, including score when it's a number. That's all.

What it does not do:

  • It does not record a score on the leaderboard or in tournaments. Call submitScore(score) for that (see Scores, leaderboards & anti-cheat). - It does not end the play session. The session ends when the player leaves the game page or closes the tab.

  • It doesn't show any UI. Your game draws its own game-over screen.

Guests / signed-in: same for both.

Runtimes: all, as long as the SDK is included.

Failure behaviour: silently ignored outside the site.

function endRound() {
  state = "over";
  if (window.GameSDK) {
    GameSDK.gameOver(score);     // lifecycle signal (and embed score bridge)
    GameSDK.submitScore(score);  // actually records the score
  }
}

Pitfalls:

  • Forgetting submitScore is the most common reason "my scores don't show up".

  • Call it once per run. Each call is forwarded to an embedding host page, which may count it.


getPlayer()

GameSDK.getPlayer(): Promise<{ signedIn: boolean; level: number; handle: string | null }>

Asks who is playing.

Parameters: none.

Returns: a promise resolving to:

Field

Type

Meaning

signedIn

boolean

true if the player is signed in to Cool GPT Games.

level

number

The player's platform-wide level (from XP across all games), not a level in your game. 1 for guests.

handle

string | null

The player's public handle, without @. null for guests.

Situation

Resolves with

Guest

{ signedIn: false, level: 1, handle: null }

Signed in

{ signedIn: true, level: <their level>, handle: "<handle>" }

Signed in, but the lookup failed

{ signedIn: true, level: 1, handle: null } (or whichever of level / handle did load)

No answer within 5 s

The last player info the SDK received (initially { signedIn: false, level: 1, handle: null })

Failure behaviour: never rejects. It falls back to the last known info after 5 seconds.

Guests / signed-in: works for both.

Limits: for a signed-in player, the first call makes two requests to the API. After a successful lookup the page remembers the answer and replies instantly, and parallel calls share one lookup. A lookup that failed isn't remembered, so the next call tries again. The remembered level stays current as awardXp answers arrive.

Runtimes: all, as long as the SDK is included.

Every answer from the page also fires the player event, including answers that arrive after the 5 second timeout. To handle a slow answer, listen to the event as well:

function showPlayer(p) {
  label.textContent = p.signedIn
    ? "Playing as " + (p.handle ? "@" + p.handle : "a signed-in player")
    : "Playing as guest. Sign in to save progress to your account.";
}

GameSDK.on("player", showPlayer);   // catches late answers too
GameSDK.getPlayer().then(showPlayer);

Pitfalls:

  • On a very slow connection the first call can still time out and resolve with the default guest info, and the real answer then arrives through the player event. Don't make permanent decisions (like wiping a guest save) based on one getPlayer() result.

  • The page doesn't push updates when a player signs in or out mid-game. Call getPlayer() again if you need fresh info (for example, when returning to your menu).

  • handle can be null even when signedIn is true.


on(event, callback)

GameSDK.on(event: string, callback: (payload?: any) => void): typeof GameSDK

Registers a listener for an SDK event.

Parameters:

Name

Type

Required

Constraints

event

string

yes

One of the event names below. Unknown names are accepted but never fire.

callback

function

yes

Receives the event payload (some events have none).

Returns: GameSDK itself, so calls can be chained.

Behaviour:

  • You can register several listeners for the same event. They run in registration order.

  • Exceptions thrown inside your callback are caught and swallowed by the SDK, so an error in one listener won't stop the others. It also won't appear as an uncaught error. Log inside your callback while debugging.

  • There's no off().

  • Events fire only in response to messages from the Cool GPT Games page, so outside the site nothing fires.

GameSDK
  .on("pause", function () { paused = true; muteAudio(); })
  .on("resume", function () { paused = false; unmuteAudio(); });

Event reference

Event

Payload

Fires when

pause

none

The game's browser tab becomes hidden

resume

none

The tab becomes visible again

adStarted

none

An ad starts showing over your game

adComplete

{ rewarded: boolean }

An ad finishes or is closed

adError

{ reason: string }

An ad couldn't be shown

xp

{ granted, total, level, leveledUp }

The page answers an awardXp() call

levelUp

{ level: number }

The player's platform level goes up (once per level)

achievement

{ key, name, alreadyHad: false, unlocked: true }

An unlockAchievement() call newly unlocked the achievement

player

{ signedIn, level, handle }

The page answers getPlayer()

friendAdded

{ handle, status }

A social.addFriend() call succeeded

tournamentJoined

result of tournaments.join()

A tournaments.join() call succeeded

purchase

result of economy.purchase()

An economy.purchase() call succeeded

replaySeed

string

The server-issued replay seed arrives

pause

No payload. The page sends it when the page's own browser tab becomes hidden: the player switches tabs, minimises the window, or locks their phone.

It is not sent:

  • when an ad is shown (use adStarted for that),

  • when the window merely loses focus but stays visible,

  • when the game first loads.

Stop your game loop, timers and audio. The browser will also slow down or stop requestAnimationFrame in a hidden tab anyway, so the main job is freezing game time so the player doesn't lose a run while away.

resume

No payload. The page sends it when the tab becomes visible again. Resume only what pause stopped. If your game loop measures time between frames, reset its "last frame" timestamp so the gap isn't treated as one huge frame.

var paused = false, last = performance.now();
GameSDK.on("pause", function () { paused = true; });
GameSDK.on("resume", function () { paused = false; last = performance.now(); });

function frame(now) {
  var dt = Math.min((now - last) / 1000, 0.1); // also cap dt as a safety net
  last = now;
  if (!paused) update(dt);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

adStarted

No payload. An ad is now showing over your game, either a mid-roll (a short interstitial) or a rewarded ad card. Pause gameplay and mute audio. Nothing else pauses your game during an ad.

adComplete

Payload: { rewarded: boolean }. The ad finished or the player closed it. Resume gameplay and audio.

  • For a mid-roll, rewarded is always false.

  • For a rewarded ad, rewarded is true only when the reward was confirmed. Grant the in-game reward only when it's true.

Each ad request ends with exactly one of adComplete or adError, never both. Full details are on Ads & rewards.

adError

Payload: { reason: string }. No ad was shown. reason is one of:

Reason

Meaning

"no_consent"

The player hasn't accepted ads (or hasn't answered the consent banner yet).

"no_fill"

No ad was available.

"no_session"

A mid-roll was requested before a play session existed (call start() first).

"busy"

Another ad is already running.

"timeout"

The page didn't finish the ad within 60 seconds (fired by the SDK itself).

"error"

Something else went wrong.

Continue the game as if the ad had finished.

xp

Payload: { granted: number, total: number, level: number, leveledUp: boolean }. Fires for every answer the page sends to awardXp(), including answers where nothing was granted (guest player, no session yet, caps reached). It doesn't fire when the call times out. Check granted > 0 before celebrating. See XP, achievements & social.

levelUp

Payload: { level: number }, the player's new platform level. Fires once per level when an XP grant (from awardXp or from an achievement's XP bonus) levels the player up.

GameSDK.on("levelUp", function (e) { showLevelBanner(e.level); });

The site already shows its own level-up toast, so an in-game banner is optional.

achievement

Payload: { key: string, name: string | undefined, alreadyHad: false, unlocked: true }. Fires only when an unlockAchievement(key) call newly unlocked the achievement for the player, the same moment the site shows its own toast. It doesn't fire when the player already had it or when nothing was unlocked. See XP, achievements & social.

player

Payload: { signedIn: boolean, level: number, handle: string | null }. Fires every time the page answers a getPlayer() call, including answers that arrive after that call's 5 second timeout. The page doesn't send it on its own (for example, on sign-in).

friendAdded

Payload: { handle: string, status: "outgoing" | "friends" }. Fires only when social.addFriend(handle) succeeded: a request was sent ("outgoing") or the two players are now friends ("friends"). It doesn't fire for "self" or "error". See XP, achievements & social.

tournamentJoined

Payload: what tournaments.join(id) resolved with. Fires only when the join succeeded, never for a null or { error } answer. See Tournaments.

purchase

Payload: what economy.purchase(sku) resolved with (the { ok: true, ... } success object, which includes the alreadyOwned: true case). Fires only when the purchase succeeded, never for an { error } answer or a null timeout. See In-game economy.

replaySeed

Payload: the seed string. Fires when the server-issued replay seed arrives, which happens after start() (or replay.ready()) once a play session exists. It may fire more than once, because each start() call re-sends the seed. It never fires while the platform is re-running your game to verify a replay. See Scores, leaderboards & anti-cheat.


Related

Was this page helpful?