NBTiktokDOCS / Developer documentation

v2 · Developer guide

DOC / 03

Leaderboard API

Game-level seasons, ten-field score mutations, regional and global ranks, and the Debug sandbox.

The Leaderboard API lets an authorized game client discover boards, inspect seasons, write ten-dimensional scores, and query regional or global rankings. Its public base URL is:

https://<platform-origin>/openapi/v2

Every HTTPS request uses the short-lived credentials.gameApi.token returned by game-client login and exchanges JSON except where noted. Stable validation errors use { "error": { "code": "ERROR_CODE", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }. Do not use container ports, /internal/*, or /admin/api/*. See the Interactive Game Integration API for login, Session, refresh, and WebSocket contracts.

#Quick integration

  1. Call POST /api/game-client/v2/login with mode: "production" or mode: "debug".
  2. Read credentials.gameApi.token and credentials.gameApi.expiresAt.
  3. Discover boards with GET /openapi/v2/leaderboards.
  4. Submit scores only while currentSeason is present.
  5. Include score1 through score10 for every player and create a unique requestId per player mutation.
  6. Query a ranking list or one player's rank.
  7. At the game Session's refreshAt, atomically switch to the new Game API Token.

#Public routes and authentication

CapabilityMethod / path
Discover boardsGET /openapi/v2/leaderboards
List simulation playersGET /openapi/v2/leaderboards/simulation-players
List board seasonsGET /openapi/v2/leaderboards/:boardKey/seasons
Write ten-dimensional scoresPOST /openapi/v2/leaderboards/:boardKey/score-mutations
List rankingsGET /openapi/v2/leaderboards/:boardKey/seasons/:seasonKey/rankings
Get one player's rankGET /openapi/v2/leaderboards/:boardKey/seasons/:seasonKey/players/:playerId/rank
List current-week rankingsGET /openapi/v2/leaderboards/:boardKey/weeks/current/rankings
Get one player's current-week rankGET /openapi/v2/leaderboards/:boardKey/weeks/current/players/:playerId/rank

All requests include:

Authorization: Bearer <gameApiToken>
Accept: application/json

The Token is a short-lived Core-signed JWT bound to game, streamer, and mode, and Game verifies it locally. Never persist or log it. Production maps to the production score space; Debug maps to sandbox.

Login returns a Session context plus the Game API credential:

{
  "session": {
    "gameId": "game_one",
    "mode": "production",
    "streamerId": "9f833c5b-ec62-4530-9ae4-0af8130a26b6"
  },
  "credentials": {
    "gameApi": {
      "token": "signed-game-api-token",
      "expiresAt": "2026-08-19T12:10:00.000Z"
    }
  }
}

The Token locks gameId, space, streamerId, regionCode, authorizationId, and Session. Dealer members inherit their dealer region; personal authorizations use the profile region. Supported write regions are SG, MY, TH, ID, PH, and TW; a client cannot override its write region. The Token is valid for at most 10 minutes and can be shortened by Session absolute expiry.

After POST /api/game-client/v2/session/refresh, atomically install the new Session access, refresh, and Game API Tokens and immediately stop actively using the old Game API Token. Its security validity lasts until its own 10-minute exp, or ends earlier when Game receives the Session revocation projection; refresh alone does not revoke it immediately.

#Data model

Each board has a stable boardKey, title, sort direction, supported score fields, available regions, and possibly a currentSeason. Ascending boards place lower scores first; descending boards place higher scores first.

Score writes do not accept seasonKey. The server resolves the active season at request time; when none is active, the write returns 409 CURRENT_SEASON_UNAVAILABLE.

Every mutation must contain exactly these ten fields:

score1, score2, score3, score4, score5,
score6, score7, score8, score9, score10

Scores are signed 64-bit integers transported as decimal strings to avoid JavaScript precision loss. Parse with a 64-bit integer or BigInt, not number. Every value, operation result, and global aggregate must stay between -9223372036854775808 and 9223372036854775807.

Each write changes only the Token regionCode. For every score field, global means the player's best regional value: desc selects the maximum, asc selects the minimum, and a tie selects the lexicographically smallest regionCode. Game recomputes all ten best sources in the same transaction. Global ranking items return that source regionCode; regional queries return the requested region.

Production regional scores are isolated by game × board × season × player × regionCode, then Game derives each player's global best. Sandbox adds streamer × game isolation. The spaces share board and season definitions but never scores or idempotency records.

The season is a canonical game-level timeline: every board transitions together using one scoreRetentionBasisPoints value (0..10000, default 50 means 0.5%). All ten Production score fields retain that percentage and truncate toward zero; Game recomputes global best values and source regions. The same transaction resets Production player attributes in all six regions according to published seasonReset rules. Debug scores are not copied and Sandbox attributes are not reset. Closing a season without transitioning resets neither scores nor attributes. Saving the automatic rule updates timing only for automatic seasons; a current manual season keeps its explicit end time, after which the automatic rule takes over.

#Discover boards

GET /openapi/v2/leaderboards
Authorization: Bearer <gameApiToken>
{
  "space": "production",
  "scoreFields": [
    "score1", "score2", "score3", "score4", "score5",
    "score6", "score7", "score8", "score9", "score10"
  ],
  "gameId": "game_one",
  "boards": [
    {
      "boardKey": "main",
      "displayName": "Total score",
      "scoreOrder": "desc",
      "currentSeason": {
        "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112",
        "seasonKey": "auto-20260819000000-r3",
        "startsAt": "2026-08-19T00:00:00.000Z",
        "endsAt": "2026-08-20T00:00:00.000Z",
        "status": "active"
      }
    },
    {
      "boardKey": "speedrun",
      "displayName": "Fastest completion",
      "scoreOrder": "asc",
      "currentSeason": null
    }
  ]
}

boards is ordered by boardKey. currentSeason: null permits historical reads but prevents public writes. Discover again after login or refresh; do not hard-code board keys, sort order, or season timing.

#Simulation players and seasons

Debug clients can list the current streamer's simulation players:

GET /openapi/v2/leaderboards/simulation-players
Authorization: Bearer <gameApiToken>
{
  "players": [
    { "playerId": "debug-player-1", "displayName": "Debug Player 1" }
  ]
}

The server ensures that the current streamer × game has preset simulation players and also returns custom players. A valid Production Token may call this route too, but the result is always Sandbox players and must not be treated as Production identities. Production score mutations should use canonical payload.player.sourcePlayerId; ranking responses return only playerId, not the live nickname.

List seasons for one allowlisted board:

GET /openapi/v2/leaderboards/main/seasons?limit=50
Authorization: Bearer <gameApiToken>

limit is optional from 1–100 and defaults to 50. cursor is the opaque value from the preceding page and is valid only for the same board.

{
  "seasons": [
    {
      "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112",
      "seasonKey": "auto-20260819000000-r3",
      "status": "active",
      "source": "automatic",
      "scoreRetentionBasisPoints": 50,
      "startsAt": "2026-08-19T00:00:00.000Z",
      "endsAt": "2026-08-20T00:00:00.000Z"
    }
  ],
  "nextCursor": null
}

Seasons are newest first. status is scheduled, active, or ended; source is manual or automatic; scoreRetentionBasisPoints is the score-retention snapshot saved for that season. A route seasonKey may be a real key or current; ended seasons remain readable but cannot be publicly mutated.

#Write scores

POST /openapi/v2/leaderboards/main/score-mutations
Authorization: Bearer <gameApiToken>
Content-Type: application/json
{
  "mutations": [
    {
      "requestId": "533b2994-b638-45a8-9db2-f0fc926fe9e5",
      "playerId": "tiktok-user-id",
      "playerName": "Alice",
      "operation": "increment",
      "scores": {
        "score1": "100",
        "score2": "0",
        "score3": "0",
        "score4": "0",
        "score5": "0",
        "score6": "0",
        "score7": "0",
        "score8": "0",
        "score9": "0",
        "score10": "0"
      }
    }
  ]
}

The request must contain 1–20 players and remain at or below 128 KiB. Every requestId is a UUID v1–v5, may not repeat within a batch, and should be globally unique. playerId is a non-empty opaque string of at most 256 characters. Each scores object contains exactly score1 through score10, with signed decimal integer strings and no missing or additional keys. operation is one of:

Production additionally requires a trimmed 1–200 character playerName, which Game persists for that player and region. Sandbox must omit playerName and uses the registered simulation player's name.

  • increment: add each submitted value;
  • set: replace each current value;
  • max: retain the larger of current and submitted values.

One operation applies to all ten fields. To increment only some fields, submit "0" for the rest. set replaces all ten values and max compares all ten; omitted-field-means-unchanged does not exist. The obsolete single-field shape using scoreField and value returns 400 INVALID_MUTATIONS.

The whole batch runs serially in one database transaction. Any invalid mutation, score, or idempotency check rolls back the batch. Concurrent writes for the same player and region are serialized; regional and global values change together.

{
  "season": {
    "seasonId": "018f2474-0c41-7bd8-9caf-2676c116b112",
    "seasonKey": "auto-20260819000000-r3",
    "startsAt": "2026-08-19T00:00:00.000Z",
    "endsAt": "2026-08-20T00:00:00.000Z",
    "status": "active"
  },
  "mutations": [
    {
      "requestId": "533b2994-b638-45a8-9db2-f0fc926fe9e5",
      "playerId": "tiktok-user-id",
      "displayName": "Alice",
      "regionCode": "SG",
      "operation": "increment",
      "scores": {
        "score1": "1280",
        "score2": "0",
        "score3": "0",
        "score4": "0",
        "score5": "0",
        "score6": "0",
        "score7": "0",
        "score8": "0",
        "score9": "0",
        "score10": "0"
      }
    }
  ]
}

season.seasonKey is the actual resolved season. Each result includes persisted displayName and the Token regionCode; scores are the new regional values, not global best values.

#Idempotency and retries

requestId identifies one player's ten-field mutation. In Production, its idempotency scope covers all Production data. In Sandbox, the scope is game × streamer. Generate a globally unique UUID for every new mutation in both modes.

  • Repeating the identical request in the same scope returns the original result without another write.
  • Reusing that requestId while changing game, player, operation, region, or any score returns 409 IDEMPOTENCY_CONFLICT.
  • After a timeout or transient server failure, retry the exact original mutation with the same requestId.
  • Never hide a conflict by generating a new UUID for data that may already have committed.

A new request resolves the current season at that time. Even after a season transition, an identical requestId and payload replay returns the original resolved season and mutation result without touching the new season. A batch that mixes new mutations with historical replays, or replays from different original seasons, returns 409 IDEMPOTENCY_CONFLICT and rolls back completely. A duplicate requestId inside one HTTP batch returns 400 INVALID_MUTATIONS. Persist the original request and reconcile rather than changing UUID. For a reliable live event, a single-player mutation may use eventId as requestId; for multiple players, derive a distinct deterministic UUID for each. ACK the source event only after the score write succeeds.

#Ranking list

Global example:

GET /openapi/v2/leaderboards/main/seasons/current/rankings?scoreField=score2&scope=global&limit=100
Authorization: Bearer <gameApiToken>

scoreField is required and accepts only score1score10. scope is global or region; regional queries also require a supported region. limit is capped at 100. Supply an opaque cursor from the previous page to continue.

{
  "scoreField": "score2",
  "scoreOrder": "desc",
  "rankings": [
    {
      "rank": 1,
      "playerId": "tiktok-user-id",
      "regionCode": "SG",
      "score": "980"
    }
  ],
  "nextCursor": null,
  "space": "production"
}

Each space + game + Sandbox streamer + board + season + scope + region + score field exposes at most the first 100 ordered rows; limit paginates only within those rows. Equal scores share an SQL RANK, so positions can skip, while playerId provides stable tie ordering. With scoreOrder=asc, lower values rank higher.

A cursor is bound to its game, space, Sandbox streamer, season, scope, region, and score field. Never parse, edit, or reuse it after changing a filter. A mismatched context returns 400 INVALID_CURSOR.

#One player's rank

GET /openapi/v2/leaderboards/main/seasons/current/players/tiktok-user-id/rank?scoreField=score2&scope=region&region=SG
Authorization: Bearer <gameApiToken>

scoreField defaults to score1 on this endpoint; scope defaults to global. Regional rank requires region. Rank is computed on the complete matching board, not only the first list page.

{
  "ranking": {
    "rank": 12,
    "playerId": "tiktok-user-id",
    "regionCode": "SG",
    "scoreField": "score2",
    "score": "5500"
  }
}

URL-encode boardKey, seasonKey, and playerId. When the player has no row, the successful response is { "ranking": null }. This endpoint computes against the complete board and can return a rank beyond 100.

#Current-week rankings

The current week runs from Monday 00:00 to the next Monday 00:00 in Asia/Shanghai. Weekly scores start from zero and independently apply the same accepted increment, set, or max operation. Production admin adjustments apply as weekly increments; idempotent replays do not update the week twice. Weekly scores continue across season transitions and ignore season carryover or reset operations.

Use GET /openapi/v2/leaderboards/main/weeks/current/rankings?scoreField=score2&scope=global&limit=100 for the list and GET /openapi/v2/leaderboards/main/weeks/current/players/tiktok-user-id/rank?scoreField=score2 for one player. Filters, top-100 pagination, SQL RANK, sort direction, and best-region behavior match season rankings. The player endpoint defaults scoreField to score1 and computes against the complete weekly board.

Both responses return space and a week object with startsAt, endsAt, timeZone, isPartial, and trackingStartedAt. isPartial is true only for the deployment week when tracking began after its Monday boundary; it becomes false for the next complete week. Old mutations are not guessed or backfilled. Production and Sandbox remain Token-selected and isolated, and current-week reads do not require an active season.

#Debug Sandbox

A Debug login always maps leaderboard responses to:

space = sandbox

Sandbox scores, global aggregates, and requestId records are isolated by streamer × game. They remain available across a new Debug Session for the same streamer and game, but are not shared across streamers or games. Board and season configuration still comes from platform administration. Discover simulation players first and stop if a response space does not match the current login mode.

#Errors and recovery

HTTPErrorClient action
401INVALID_ACCESS_TOKEN / INVALID_ACCESS_TOKENStop and replace credentials through Session refresh or login
400INVALID_REGIONUse a supported region; write region comes from account authority
400INVALID_LIMITUse the endpoint's documented pagination range
400INVALID_SCORE_FIELDUse score1score10; ranking lists require it explicitly
400INVALID_MUTATIONS / INVALID_SCORECorrect the full ten-field mutation; do not retry unchanged
400REGION_REQUIREDAdd region for a regional rank query
400INVALID_CURSORDrop the cursor and start from the first page
404SEASON_NOT_FOUNDRefresh season authority and check seasonKey
409CURRENT_SEASON_UNAVAILABLEWait for an active season or contact an administrator
409SEASON_NOT_ACTIVERefresh board and season state
409IDEMPOTENCY_CONFLICTCompare the original request and season; do not write again
413Request body too largeSplit into at most 20 players and no more than 128 KiB
500INTERNAL_ERRORPreserve requestId, record context, and contact the platform

Use exponential backoff with jitter for reads after network or 5xx errors. Retry a write only with the exact original context, content, and requestId. If the Game API Token expires, perform one serialized game Session refresh and use the new Token; never replay an uncertain one-use refresh Token.

#Release checklist

  • Keep credentials.gameApi.token in memory and rotate it with the game Session.
  • Validate boardKey, current season, score fields, regions, and space before writing.
  • Send exactly ten decimal-string scores for every player.
  • Use one stable UUID per logical mutation and replay it only with identical data.
  • Treat Production and Sandbox as separate spaces.
  • Bind cursors to the exact query that created them.
  • Test absent seasons, 20-player / 128 KiB limits, idempotent retries, conflicts, regional/global ranks, and Token expiry.
  • Log only non-secret context such as gameId, boardKey, actual seasonKey, status, and requestId; never log an Authorization header.