InZoneInZone
02 — Integrate

Eight endpoints. Under thirty minutes.

Score, leaderboard, challenge, progress, chat, save state and coins — each line of code lights a feature in the live game. The reference key for your game is on the Upload screen. Everything on this page is also in the downloadable guide.

Download the full guide (.md)same content, one offline file for your team
RUNTIMEwindow.__INZONE_SOCIAL_LOOP_CONFIG__

How your game connects

Your HTML game loads inside the InZone app in a WebView. InZone injects two things into your JavaScript environment: a config object with the player's identity, your API key and the backend URL — and window.InZoneSDK, a bridge whose methods call the backend for you, so your game never constructs URLs or sets auth headers.

Both arrive after your page finishes loading, via the inzone:sdk-ready event. Never wait with a timeout, and never fall back to default values — if the config never arrives, your game is not running inside InZone.

gameIdyour registered game identifier
gameNamehuman-readable game name
gameKeyAPI key for protected endpoints (coins, game-state)
sessionIdcurrent play session ID
userIdthe player's InZone user ID (Firebase UID)
backendBaseUrlthe backend URL — never hardcode one
fixtureModetrue when running locally without a real backend
game-sdk/connect.js
// InZone injects the config + SDK AFTER your page
// (and every script/font/image) finishes loading.
// Never wait with a timeout — listen for the event:
function waitForConfig() {
  return new Promise((resolve) => {
    if (window.__INZONE_SOCIAL_LOOP_CONFIG__) {
      return resolve(window.__INZONE_SOCIAL_LOOP_CONFIG__);
    }
    window.addEventListener('inzone:sdk-ready',
      (e) => resolve(e.detail), { once: true });
  });
}

const config = await waitForConfig();
// …or simply:
const config = await window.InZoneSDK.getConfig();
InZoneSDK bridge methods — every call returns a Promise with the endpoint's JSON
InZoneSDK.getConfig()resolves with the config object
InZoneSDK.postScore(payload)POST /post-score
InZoneSDK.sendChallenge(payload)POST /send-challenge + native share sheet
InZoneSDK.openChat(payload)POST /open-chat
InZoneSDK.gameState(payload)GET /game-state
InZoneSDK.purchaseCoinTier(coins, payload)POST /coins/tier-{coins}
InZoneSDK.close()exits the game, returns to InZone

Endpoints without a bridge method yet — GET/POST /state, POST /progress/share and GET /leaderboard — use direct fetch calls with config.backendBaseUrl, as shown on their cards below.

POST/api/game-sdk/post-score

Post Score

Call on game-over, round-end or level-complete. Records the score, writes a leaderboard entry, and returns the player rank, a top-10 snippet and ready-made share data.

runsrankedshare-data
SDK bridge · InZoneSDK.postScore(payload)
Live · responding 200 OK
game-sdk/post-score.js
// Request fields:
//   gameId: string (required)
//   score: number (required)
//   playerId: string — omit for anonymous
//   gameName: string — falls back to gameId
//   durationMs: number — round duration in ms
//   sessionId: string
//   platform: string — e.g. "flutter-webview"
//   playerName: string — falls back to "Player"
//   metadata: object — arbitrary key-value pairs

const data = await InZoneSDK.postScore({
  score: 14820,
  durationMs: 92447,
  playerName: 'ProGamer42',
  metadata: { lap: 3 },
});

// Response types:
//   data.player.rank: number | null
//   data.score.value: number
//   data.score.best: number
//   data.leaderboard.entries: Array<{
//     rank: number, playerId: string,
//     displayName: string, score: number }>
//   data.share.title: string
//   data.share.url: string
POST/api/game-sdk/send-challenge

Challenge

Creates a 24-hour duel and generates a share card in one call — the old standalone share-card endpoint no longer exists. Includes a deep link (inzone://game?gameId=…, delivered via an AppsFlyer OneLink with deep_link_value=community_game) that opens the community game on the Game Hub inside InZone.

socialduel24hdeep-link
SDK bridge · InZoneSDK.sendChallenge(payload)
Live · responding 200 OK
game-sdk/challenge.js
// Request fields:
//   gameId: string (required)
//   senderId: string (required) — challenger's userId
//   recipientId: string — omit for open challenge
//   score: number
//   message: string — default "Can you beat this score?"
//   sessionId: string
//   challengeType: string — default "duel"
//   expiresHours: number — default 24
//   title: string — auto-generated from score if omitted
//   template: string — default "default"
//   shareUrl: string — default AppsFlyer OneLink (deep_link_value=community_game, af_dp=inzone://game?gameId={gameId})
//   imageUrl: string | null

const data = await InZoneSDK.sendChallenge({
  senderId: config.userId,
  recipientId: 'friend_abc',
  score: 14820,
  message: 'Can you beat this score?',
});

// Response types:
//   data.challenge.challengeId: string
//   data.challenge.gameDeepLink: string
//   data.challenge.expiresAt: string (ISO 8601)
//   data.challenge.status: "pending"
//   data.share.url: string
//   data.share.gameDeepLink: string
//   data.share.text: string
POST/api/game-sdk/progress/share

Progress

Shareable snapshot of an achievement, high score or milestone — without creating a challenge. Use for "Share Progress" / "Brag" buttons. No bridge method — use direct fetch.

feedvisualshare
direct fetch · no bridge method yet
Live · responding 200 OK
game-sdk/progress.js
// Request fields:
//   gameId: string (required)
//   userId: string (required)
//   score: number
//   title: string — auto-generated from score
//   message: string — default "Check out what I just did"
//   sessionId: string
//   visual: string — default "auto"
//   metrics: object — arbitrary stats for share card
//   achievements: string[] — achievement IDs/names
//   template: string — default "progress"
//   imageUrl: string | null
//   shareUrl: string — default AppsFlyer OneLink (deep_link_value=community_game, af_dp=inzone://game?gameId={gameId})

const config = window.__INZONE_SOCIAL_LOOP_CONFIG__;
const res = await fetch(config.backendBaseUrl
  + '/api/game-sdk/progress/share', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    gameId: config.gameId,
    userId: config.userId,
    score: 14820,
    title: 'New high score — 14820!',
    metrics: { kills: 15, accuracy: 0.82 },
    achievements: ['sharpshooter'],
  }),
});
const data = await res.json();
// data.shareCard.shareCardId: string
// data.share.title: string
// data.share.url: string
// data.shareTargets: string[]
POST/api/game-sdk/open-chat

Chat

Opens or joins the per-game group chat thread and sends a message. If no message is provided, one is built from context (score, wave, result). InZone renders the chat UI — your game just ensures the thread exists and the player is in it.

groupchatlivesocial
SDK bridge · InZoneSDK.openChat(payload)
Live · responding 200 OK
game-sdk/chat.js
// Request fields:
//   gameId: string (required, unless threadId given)
//   threadId: string — reuse a specific thread
//   userId: string
//   sessionId: string
//   characters: string[] — AI character names
//   context: object — { score, result, wave, gameName }
//   message: string — auto-built from context if omitted

const data = await InZoneSDK.openChat({
  context: { score: 14820, result: 'win', wave: 5 },
  characters: ['nova', 'orin'],
});

// Response types:
//   data.conversation.conversationId: string
//   data.conversation.participants: string[]
//   data.conversation.characters: string[]
//   data.conversation.context: object
GET/api/game-sdk/game-stateX-GAME-KEY

Game State

Account overview: coin balance, last 50 transactions and last 50 scores, newest first. Call on game load before offering purchases. Requires X-Game-Key header.

wallethistoryprotected
SDK bridge · InZoneSDK.gameState(payload)
Live · responding 200 OK
game-sdk/game-state.js
// Request fields (query params):
//   gameId: string (required)
//   userId: string (required)
// Header: X-Game-Key (required)

const data = await InZoneSDK.gameState({});

// Response types:
//   data.data.balance: number — coin count
//   data.data.currency: "Coin"
//   data.data.transactions: Array<{
//     transactionId: string, title: string,
//     coins: number, commissionCoins: number,
//     developerCoins: number, status: string,
//     createdAt: string }>
//   data.data.scores: Array<{
//     scoreId: string, score: number,
//     durationMs: number, displayName: string,
//     createdAt: string }>
GET / POST/api/game-sdk/state

Save / Load

Per-player save blob — progress, inventory, checkpoints. One slot per player per game; each POST overwrites it, max 256 KB, state must be a JSON object. version auto-increments; version 0 means a new player (never a 404). NOT for coin balances.

save-slot256kbversioned
direct fetch · no bridge method yet
Live · responding 200 OK
game-sdk/save-load.js
// Load — GET query params:
//   gameId: string (required)
//   userId: string (required)
// Save — POST body:
//   gameId: string (required)
//   userId: string (required)
//   state: object (required) — max 256 KB
//   metadata: object — { saveLabel, platform, … }

const config = window.__INZONE_SOCIAL_LOOP_CONFIG__;
const base = config.backendBaseUrl + '/api/game-sdk';

// Load response types:
//   data.data.state: object
//   data.data.version: number — 0 = new player
//   data.data.metadata: object
//   data.data.updatedAt: string | null
const res = await fetch(base + '/state'
  + '?gameId=' + config.gameId
  + '&userId=' + config.userId);
const save = (await res.json()).data;

// Save response types:
//   data.data.version: number (auto-incremented)
//   data.data.bytes: number
//   data.data.savedAt: string
await fetch(base + '/state', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    gameId: config.gameId,
    userId: config.userId,
    state: { level: 12, inventory: ['shield'] },
    metadata: { saveLabel: 'Checkpoint Level 12' },
  }),
});
GET/api/game-sdk/leaderboard

Leaderboard

Full standings for a game, ordered by score descending. post-score already returns a top-10 snippet — use this for a standalone leaderboard screen or more entries. No bridge method — use direct fetch.

globalrankedread-only
direct fetch · no bridge method yet
Live · responding 200 OK
game-sdk/leaderboard.js
// Request fields (query params):
//   gameId: string (required)
//   limit: number — default 50, max 200
//   scope: string — default "global"

const config = window.__INZONE_SOCIAL_LOOP_CONFIG__;
const res = await fetch(config.backendBaseUrl
  + '/api/game-sdk/leaderboard'
  + '?gameId=' + config.gameId + '&limit=20');
const data = await res.json();

// Response types:
//   data.totalEntries: number
//   data.scope: string
//   data.entries: Array<{
//     rank: number, entryId: string,
//     playerId: string, playerName: string,
//     score: number, metadata: object,
//     createdAt: string }>
POST/api/game-sdk/coins/tier-{10,50,150,400}X-GAME-KEY

Coins

Purchase at one of four fixed tiers, debited from the player's InZone balance. Purchases are atomic: on success the coins are already deducted; after a network failure, reconcile via game-state. Requires X-Game-Key header.

microtx4-tier90% rev shareprotected
SDK bridge · InZoneSDK.purchaseCoinTier(coins, payload)
Live · responding 200 OK
game-sdk/coins.js
// Request fields:
//   userId: string (required)
//   gameId: string (required)
//   title: string (required) — shown in history
//   description: string — defaults to title
//   sessionId: string
// Header: X-Game-Key (required)

try {
  const data = await InZoneSDK.purchaseCoinTier(10, {
    title: 'Extra attempt',
    description: 'One more run',
  });

  // Response types:
  //   data.data.transactionId: string
  //   data.data.coins: number
  //   data.data.newBalance: number
  //   data.data.commissionCoins: number
  //   data.data.developerCoins: number
  //   data.data.commissionRate: number (0.1)
  //   data.tier.name: string
  updateCoinDisplay(data.data.newBalance);
  startNewRound();
} catch (err) {
  // INSUFFICIENT_BALANCE → err.details has
  //   currentBalance: number, required: number
  showMessage(err.message);
}
AuthenticationX-Game-Key

Protected endpoints — GET /game-state and all POST /coins/* tiers — require your game key. Find it on the Settings page under Server key · Social Loops; it also arrives in config.gameKey at runtime. If your game doesn't have a key yet, one is generated automatically the first time you open Settings or the first time the game loads inside InZone. The SDK bridge attaches it automatically. Everything else — post-score, send-challenge, progress/share, open-chat, state, leaderboard — needs no key.

game-sdk/auth.js
// Pass the key as the header AND in the
// body / query string — the backend checks both.
await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Game-Key': config.gameKey,
  },
  body: JSON.stringify({
    gameKey: config.gameKey,
    ...payload,
  }),
});
Coin tiers90% to you · 10% commission
tier-10Impulse · 10 coinsretries, small boosts, one-session cosmetics
tier-50Investment · 50 coinspower-ups, hard modes, leaderboard entry fees
tier-150Identity · 150 coinsskins, permanent abilities, exclusive modes
tier-400Momentum · 400 coinsseason passes, full game unlocks, bundles

Players earn coins in the InZone app — your game spends them. Purchases are atomic: if the response says success: true, the coins are already deducted, and data.newBalance is the number to put on screen. If the network drops before the response arrives, reconcile with game-state.

Response format · error codesevery response is JSON
game-sdk/error-shape.json
// Success responses always include
{ "success": true, … }

// Errors follow one shape:
{
  "success": false,
  "error": {
    "code": "MISSING_GAME_ID",
    "message": "gameId is required",
    "status": 400
  }
}

// INSUFFICIENT_BALANCE also carries
// error.details.currentBalance + .required
MISSING_GAME_IDgameId was not provided
MISSING_USER_IDuserId (or playerId) was not provided
MISSING_TITLEtitle was not provided (coin purchases)
MISSING_SENDER_IDsenderId was not provided (challenges)
MISSING_GAME_OR_THREADneither gameId nor threadId (open-chat)
MISSING_GAME_KEYgameKey missing on a protected endpoint
INVALID_GAME_KEYgameKey does not match the registered key
GAME_NOT_FOUNDno game exists with this gameId
USER_NOT_FOUNDno user exists with this userId
INSUFFICIENT_BALANCEnot enough coins — details has balance + required
INVALID_COIN_TIERcoin amount is not 10 / 50 / 150 / 400
INVALID_STATEstate is not a JSON object / not serializable
STATE_TOO_LARGEstate blob exceeds 256 KB
INVALID_REQUESTcatch-all for malformed requests
INTERNAL_ERRORserver-side failure
Typical integration flowno required sequence
game-sdk/flow.txt
Game loads
  └─ await config / InZoneSDK        (inzone:sdk-ready)
  └─ InZoneSDK.gameState({})         → coin balance
  └─ GET /state                      → restore progress

Gameplay
  └─ POST /state                     → save at checkpoints

Round ends
  └─ InZoneSDK.postScore({ score })  → score + leaderboard

Game-over screen (tie each to a button)
  └─ InZoneSDK.purchaseCoinTier(10, { title })  → retry
  └─ InZoneSDK.sendChallenge({ score })         → duel a friend
  └─ POST /progress/share                       → share progress
  └─ InZoneSDK.openChat({ context })            → group chat

Leaderboard screen
  └─ GET /leaderboard                → full standings

No required sequence — call anything once the config exists.
Troubleshootingmost-seen issues
Endpoints "work" but no data reaches the backend
Your game is running on fallback/mock values. The config is injected after all resources load, so a setTimeout wait can expire first. Never wait with a timeout — use the inzone:sdk-ready event or InZoneSDK.getConfig().
Balance never loads, start button never appears
gameState requires userId and gameKey. If either is missing from window.__INZONE_SOCIAL_LOOP_CONFIG__, the call fails. Log the config object on page load to verify every field is populated.
window.InZoneSDK is undefined
Your code ran before InZone finished injecting the SDK. Wrap startup in a check for window.InZoneSDK and otherwise wait for the inzone:sdk-ready event.
Works locally, fails in production
A hardcoded localhost or test URL is hiding somewhere. Search your code for hardcoded backend URLs and replace them with config.backendBaseUrl.
Nothing works outside the InZone app
Expected — the config and SDK come from the InZone WebView. In a plain browser there is no injection and no event. To test locally, mock window.__INZONE_SOCIAL_LOOP_CONFIG__ with test values before your game code runs.
Field names: all endpoints accept camelCase and PascalCase; playerId is an alias for userId. Responses always use camelCase. Base URL: https://inzoneapi-912424781531.us-central1.run.app/api/game-sdk/* — always read it from config.backendBaseUrl, never hardcode it.backend
Integration healthlast 24h
Requests
awaiting traffic
Error rate
no errors yet
p95 latency
awaiting data