NBTiktokDOCS / Developer documentation

v2 · Developer guide

DOC / 01

Interactive Game Integration API

Email/password sign-in, client sessions, WebSocket events, reliable ACKs, and Debug testing.

Use this API to sign a game client in with an email and password, maintain its Session, optionally authenticate to an independently deployed game backend, receive live interaction events over WebSocket, acknowledge reliable events, and test the same integration in Debug mode.

Use the public HTTPS origin supplied by NBTiktok. Never connect directly to container ports, diagnostic ports, /internal/*, or administration routes.

#Integration flow

  1. Call POST /api/game-client/v2/login with the platform email, password, gameId, mode, and clientBuild; no CAPTCHA is used.
  2. For direct leaderboard or player-attribute integration, omit backend and use the returned credentials.gameApi.
  3. When independent backend authentication is enabled, sign-in returns backendAuth and credentials.gameBackend; the request still contains only the five login fields above.
  4. If backendAuth and credentials.gameBackend are present, send that JWT only to the trusted URL selected by this client build.
  5. Open the one-use WebSocket Ticket and validate the ready fence. In Production, process and persist reliable events before ACK; in Debug, acknowledge each debug_command.
  6. Rotate the credential set and renew the independent-backend socket at refreshAt; request a new platform Ticket after disconnect and end with DELETE /api/game-client/v2/session.

#Public routes

CapabilityMethod / pathAuthentication
Sign in and create SessionPOST /api/game-client/v2/loginEmail, password, game, mode, and build only
Removed device authorizationPOST /api/game-client/v2/device-authorizations and /tokenReturns 410 DEVICE_AUTHORIZATION_REMOVED
Removed CAPTCHAGET /api/game-client/v2/captchaReturns 410 CAPTCHA_LOGIN_REMOVED
Inspect SessionGET /api/game-client/v2/sessionBearer <sessionAccessToken>
Rotate credentialsPOST /api/game-client/v2/session/refreshBearer <refreshToken>; empty body
Issue WebSocket TicketPOST /api/game-client/v2/session/websocket-ticketBearer <sessionAccessToken>; empty body
Sign outDELETE /api/game-client/v2/sessionBearer <sessionAccessToken>
Event connectionGET /ws/game-client/v2?ticket=... (WebSocket Upgrade)One-use 60-second Ticket
Public game discoveryGET /api/games/v2None
Localized game guideGET /api/games/v2/:gameId/guide?locale=...None
Game gift catalogGET /api/games/v2/:gameId/giftsNone
Game screenshotGET /api/games/v2/:gameId/screenshots/:positionNone; position is 1–5
Leaderboards and player attributes/openapi/v2/*Bearer <credentials.gameApi.token>
Independent backend HTTP/WSSClient-owned trusted URLHTTP Bearer; WSS nbt.auth first frame
Backend signing keysGET /.well-known/game-backend-jwks.jsonNone; cache per Cache-Control

Except for 204 and successful binary screenshot responses, HTTP success and error bodies are JSON. Login uses the account service's flat error envelope:

{ "error": "INVALID_CREDENTIALS" }

Session, WebSocket Ticket, /openapi/v2/*, and edge rate-limit failures use the correlated V2 envelope:

{ "error": { "code": "ERROR_CODE", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }

For V2 errors, the X-Request-Id header equals error.requestId; treat it as opaque. Application errors normally use UUIDs, while an edge-generated 429 RATE_LIMITED uses a 32-character hexadecimal ID. Either envelope may include optional stable, non-sensitive details.

#Email and password sign-in

POST /api/game-client/v2/login
Content-Type: application/json
{
  "email": "streamer@example.com",
  "password": "account-password",
  "gameId": "game_one",
  "mode": "production",
  "clientBuild": 120
}

The password is submitted only to NBTiktok and must be erased from memory after the request. Never persist it in PlayerPrefs, configuration, logs, URLs, crash reports, or telemetry. The platform never places email, password, login Tokens, or Cookies in a backend JWT or sends them to an independent backend. Email and password select the account and its isolated Sandbox.

#Successful login response

{
  "session": {
    "sessionId": "6fe3890b-bb7c-4e45-b2b2-1db2d77751ce",
    "streamerId": "9f833c5b-ec62-4530-9ae4-0af8130a26b6",
    "authorizationId": "20637113-6d4e-490c-ac6d-74242cd1de85",
    "gameId": "game_one",
    "mode": "production",
    "credentialSource": "user_session",
    "credentialId": null,
    "roomId": "creator-room",
    "clientBuild": 120,
    "minimumClientBuild": 100,
    "expiresAt": "2026-08-20T12:00:00.000Z",
    "absoluteExpiresAt": "2026-08-26T12:00:00.000Z"
  },
  "credentials": {
    "sessionAccess": {
      "token": "opaque-session-access-token",
      "expiresAt": "2026-08-19T12:30:00.000Z"
    },
    "gameApi": {
      "token": "signed-game-api-token",
      "expiresAt": "2026-08-19T12:10:00.000Z"
    },
    "gameBackend": {
      "token": "signed-game-backend-token",
      "expiresAt": "2026-08-19T12:30:00.000Z"
    },
    "refresh": {
      "token": "opaque-single-use-refresh-token",
      "expiresAt": "2026-08-20T12:00:00.000Z",
      "refreshAt": "2026-08-19T12:08:00.000Z"
    },
    "websocket": {
      "url": "/ws/game-client/v2",
      "ticket": "opaque-single-use-ticket",
      "expiresAt": "2026-08-19T12:01:00.000Z"
    }
  },
  "backendAuth": {
    "audience": "urn:nbtiktok:game-backend:game_one",
    "operatorName": "Example Studio",
    "privacyPolicyUrl": "https://game.example.com/privacy",
    "configVersion": 3
  }
}

Treat IDs, room, mode, build policy, expiries, and returned paths as server authority. Tokens and Tickets must never be logged, persisted, placed in analytics, or sent to another origin.

Credential timing and concurrency rules:

  • the session access Token is valid for at most 30 minutes and the Game API Token for at most 10 minutes;
  • the refresh Token is single-use and the sliding Session lifetime is at most 24 hours;
  • absoluteExpiresAt is at most seven days after login;
  • when enabled, the game-backend JWT lasts at most 30 minutes; the credential set still normally refreshes about eight minutes after issuance because refreshAt is the earliest refresh point across all short-lived credentials;
  • a WebSocket Ticket is valid for at most 60 seconds and can be consumed once;
  • one account can hold only one Production Session; a new Production login replaces it even for a different game;
  • Debug keeps one Session per authorizationId, can coexist with Production, and a new login for that authorization replaces its older Debug Session.

Common login failures are complete below. Do not automatically retry credential failures; back off for transient failures.

HTTPerrorRequired action
400MODE_INVALIDCorrect mode
400CLIENT_BUILD_INVALIDSend a safe integer of at least 1
401INVALID_CREDENTIALSEmail is unknown, password is wrong, or the account is temporarily locked; show one generic message
403DEALER_UNAVAILABLECheck dealer and membership state
403GAME_DISABLEDStop login for the disabled game
403GAME_NOT_AUTHORIZEDObtain a valid game authorization
404GAME_NOT_FOUNDCorrect gameId
409ROOM_ID_REQUIREDAdd the live-room ID to the profile
409PROFILE_INCOMPLETEComplete the profile region
409GIFT_CREDIT_EXHAUSTEDRestore the account's gift credit before login
409BILLING_CONFIG_REQUIREDWait for Production billing configuration
426CLIENT_BUILD_UNSUPPORTEDUpgrade to the returned minimumClientBuild
429LOGIN_RATE_LIMITED / RATE_LIMITEDHonor Retry-After and back off
503AUTH_UNAVAILABLE / BILLING_BACKLOG_PAUSEDBack off exponentially and inform the user

The 426 body uses the V2 envelope: error.code is CLIENT_BUILD_UNSUPPORTED, error.requestId is the correlation ID, and error.details contains the submitted clientBuild and current minimumClientBuild.

#Independent backend routing

NBTiktok never stores or returns a business API URL. Local, test, and production builds use the same credentials.gameBackend JWT, exact per-game audience, and JWKS verification. The client reads the URL from trusted build configuration; a development build may override it locally. No URL or additional secret is sent to the login endpoint.

The response contains backendAuth with audience, operator, privacy policy, and configuration version, but no route. Send the JWT only to the locally selected trusted origin and reject cross-origin redirects.

HTTP sends this JWT as Authorization: Bearer. An independent-backend WebSocket uses the fixed nbt.game-backend.v1 subprotocol and sends the same JWT only in the first {"type":"nbt.auth","token":"..."} text frame; it never uses a URL token. Repeat that frame with the newly returned JWT after Session refresh to renew the existing connection. This is not the platform /ws/game-client/v2 connection, which continues to use the one-use credentials.websocket.ticket.

Trusted builds should configure NBT_GAME_BACKEND_HTTP_URL and NBT_GAME_BACKEND_WS_URL separately. Plaintext http/ws is allowed only for localhost, 127.0.0.0/8, and [::1]; remote test and production addresses require https/wss.

Browser Portal login uses NBTiktok email/password reauthentication plus Authorization Code and PKCE. Production callbacks exactly match the registered HTTPS URI; local callbacks are restricted to http://127.0.0.1:<port>/__nbt/callback or the IPv6 loopback equivalent.

#Retired endpoints

#Session and credential rotation

Inspect the current Session with:

GET /api/game-client/v2/session
Authorization: Bearer <sessionAccessToken>

The response contains only the session snapshot and does not return any Token. Validate identity, mode, room, build, and expiry against local state. Backend routing remains in client-owned configuration. Sandbox roomId may be null.

At refreshAt, serialize this request across all client threads:

POST /api/game-client/v2/session/refresh
Authorization: Bearer <refreshToken>

The empty-body response returns a new session, authoritative backendAuth when enabled, plus credentials.sessionAccess, credentials.gameApi, optional credentials.gameBackend, and credentials.refresh. It intentionally does not return websocket. Install the full new set atomically, stop issuing HTTP requests with old Tokens, renew an active independent-backend WebSocket with the new Backend JWT, and discard any unused platform Ticket. If gameBackend is omitted, close the independent-backend connection and stop calling it.

An already consumed refresh Token returns:

{ "error": { "code": "REFRESH_TOKEN_REUSED", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }

Reuse also revokes the Session and closes the socket with 4014 REFRESH_TOKEN_REUSED. An unknown or expired Token returns 401 REFRESH_TOKEN_INVALID; an absolute-expiry or invalid Session context returns 401 SESSION_REAUTH_REQUIRED.

Never run concurrent refreshes. If a network failure makes consumption uncertain, clear all in-memory credentials and sign in again instead of replaying that refresh Token. refreshAt is always an ISO 8601 timestamp; if it is already due when the client resumes, perform one serialized refresh immediately. On SESSION_REAUTH_REQUIRED, erase the credentials and sign in again.

#WebSocket connection and fence

Convert the public HTTPS origin to wss:// and append the returned relative path:

wss://<platform-origin>/ws/game-client/v2?ticket=<single-use-ticket>

The URL may contain only the Ticket. An invalid, consumed, or expired Ticket receives HTTP 401; billing state may produce 409; capacity or live-source failures may produce 503. After a disconnect, request a new Ticket:

POST /api/game-client/v2/session/websocket-ticket
Authorization: Bearer <sessionAccessToken>
{
  "credentials": {
    "websocket": {
      "url": "/ws/game-client/v2",
      "ticket": "new-single-use-ticket",
      "expiresAt": "2026-08-19T12:11:00.000Z"
    }
  }
}

The first business message is the connection fence:

{
  "type": "ready",
  "sessionId": "6fe3890b-bb7c-4e45-b2b2-1db2d77751ce",
  "userId": "account-user-id",
  "streamerId": "9f833c5b-ec62-4530-9ae4-0af8130a26b6",
  "authorizationId": "20637113-6d4e-490c-ac6d-74242cd1de85",
  "gameId": "game_one",
  "mode": "production",
  "credentialSource": "user_session",
  "credentialId": null,
  "roomId": "creator-room",
  "clientBuild": 120,
  "minimumClientBuild": 100,
  "leaseGeneration": 4
}

Require userId to be a UUID and leaseGeneration to be a positive integer. Reject the connection unless every other field matches current local authority. Bind every event and ACK queue to gameId plus leaseGeneration; never reuse state from an older connection. WebSocket protocol Ping/Pong supplies the heartbeat. Do not send a custom { "type": "pong" } JSON message.

#Production events

After ready, request unacknowledged reliable events. limit defaults to 100 and is capped at 200.

{
  "type": "pull",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "limit": 100
}

The server emits individual events and then continues with real-time delivery; it does not send a separate pull-complete message.

{
  "type": "event",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "event": {
    "eventId": "2d2a0ef5-2388-45de-a255-2dcc73543d3b",
    "eventType": "comment",
    "occurredAt": "2026-08-19T12:00:00.000Z",
    "payload": {
      "player": {
        "sourcePlayerId": "viewer-id",
        "displayName": "Viewer"
      },
      "content": "hello"
    }
  }
}

eventId is a UUID and occurredAt is an ISO 8601 timestamp. gameId and leaseGeneration exist only in the outer envelope. Supported eventType values are comment, like, gift, and stream_end. Event-specific payload fields include:

{
  "eventId": "3a83d2f9-2095-4821-926d-ff8a80a0e771",
  "eventType": "like",
  "occurredAt": "2026-08-19T12:00:02.000Z",
  "payload": {
    "player": { "sourcePlayerId": "viewer-id", "displayName": "Viewer" },
    "count": 12
  }
}
  • comment: player.sourcePlayerId, player.displayName, content;
  • like: player.sourcePlayerId, player.displayName, count;
  • gift: player plus gift.providerGiftId, displayName, totalCount, deltaCount, unitDiamondCount, giftCoins, missingPrice, groupId, and repeatEnd;
  • stream_end: the stream end time and source context supplied by the envelope.

For a gift streak, award from deltaCount, not cumulative totalCount; groupId may be null and repeatEnd marks the streak end. unitDiamondCount may be zero with missingPrice: true. Never infer platform billing from the client event, and do not interpret ack_result.consumed as a diamond count.

Only comment and gift are durably replayed and require ACK. like is real-time only and has no ACK. stream_end is not replayed or ACKed and is followed by close code 4007. Payloads can gain additive fields; ignore unknown fields, but never substitute a provider gift ID or player ID for canonical eventId.

#Reliable processing and ACK

ACK means the game effect and its processing record are durably complete, not merely that the socket received a message.

  1. Deduplicate by canonical eventId in durable storage.
  2. Execute the game effect once.
  3. Persist the completed status in the same recoverable workflow.
  4. Queue the ID for ACK only after both steps succeed.
  5. Leave failed events unacknowledged so Core can replay them.
  6. For a replay already completed locally, skip the effect and queue that eventId again.

Send only delivered UUIDs from the current connection, without duplicates. A batch is capped at 500 IDs; the recommended flush trigger is 25ms or 100 IDs, whichever happens first, with one in-flight batch per connection.

{
  "type": "ack",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "eventIds": ["2d2a0ef5-2388-45de-a255-2dcc73543d3b"]
}
{
  "type": "ack_result",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "acknowledged": 1,
  "consumed": 1,
  "duplicate": 0
}

acknowledged counts first-time confirmations, consumed counts events newly entering gift-consumption audit (not diamonds), and duplicate counts events confirmed earlier. All are non-negative integers and must satisfy acknowledged + duplicate === submitted eventIds length and consumed <= acknowledged. A fully replayed batch may validly return acknowledged = 0, duplicate = N. An unmatched or invalid result is protocol desynchronization: close, clear the in-memory ACK queue, request a new Ticket, and recover from replay plus durable eventId state.

#Debug protocol

Debug uses the same login, refresh, Ticket, ready, and fence rules. Commands arrive as:

{
  "type": "debug_command",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "commandId": "79ac82bb-a842-4817-aeb8-cf345db37639",
  "requiresAck": true,
  "transient": true,
  "event": {
    "eventId": "75c93fa7-3e1e-4fbc-b736-37b21c49e3d6",
    "eventType": "gift",
    "occurredAt": "2026-08-19T12:00:03.000Z",
    "payload": {
      "player": { "sourcePlayerId": "debug-player", "displayName": "Debug Player" },
      "gift": {
        "providerGiftId": "5655",
        "displayName": "Rose",
        "totalCount": 1,
        "deltaCount": 1,
        "unitDiamondCount": 1,
        "giftCoins": 1,
        "missingPrice": false,
        "groupId": null,
        "repeatEnd": true
      }
    }
  }
}

Always acknowledge the outcome explicitly:

{
  "type": "debug_command_ack",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "commandId": "79ac82bb-a842-4817-aeb8-cf345db37639",
  "outcome": "processed"
}

For failure, send "outcome": "failed" and an errorCode of 1–128 characters, for example EFFECT_REJECTED; a processed result must omit errorCode. Core confirms with type: "debug_command_ack_received" and status: "processed" or "failed". Debug commands are transient, are not replayed or billed, and should normally be answered within 15 seconds. A Debug client must not send Production pull or ack.

#Errors, gift catalog, and sign out

Protocol errors use { "type": "error", "error": "ERROR_CODE" }. Treat SESSION_FENCE_STALE, ACK_EVENT_INVALID, ACK_FAILED, and Debug fence errors as reasons to disconnect and recover from persisted state.

CloseExample reasonContinue SessionClient action
4001SESSION_REPLACEDNoStop the old client and erase credentials
4002CLIENT_LOGOUTNoNormal sign-out completed
4003SESSION_INVALID / SESSION_EXPIRED / SESSION_REAUTH_REQUIREDNoSign in again
4004AUTHORIZATION_INVALID / AUTHORIZATION_EXPIREDNoRepair authorization, then sign in
4005GAME_DELETED / GAME_DISABLEDNoStop the game and report its state
4006CLIENT_BUILD_UNSUPPORTEDNoUpgrade the client
4007STREAM_ENDEDNoEnd this Production run
4008SOURCE_BINDING_CHANGEDNoRefresh profile authority, then sign in
4009Administrative or release shutdownNoSurface the reason and sign in again if appropriate
4010SLOW_CONSUMERYesRemove processing blockage, then reconnect with backoff
4011SOURCE_RELEASEDYesRequest a new Ticket after backoff
4012BALANCE_EXHAUSTEDNoErase credentials and restore credit
4013HEARTBEAT_TIMEOUTYesCheck the network and reconnect with backoff
4014REFRESH_TOKEN_REUSEDNoErase all credentials and sign in again

Only 4010, 4011, and 4013 are reconnectable. Use bounded exponential backoff with random jitter; treat all other listed codes as Session termination.

The public gift catalog is informational and requires no authentication:

GET /api/games/v2/game_one/gifts
Accept: application/json
{
  "game": { "gameId": "game_one", "displayName": "Example Game" },
  "gifts": [
    {
      "key": "rose-burst",
      "catalogGiftId": "5e86a488-1baf-48f2-8b75-d0f8e998ee81",
      "providerGiftId": "5655",
      "displayName": "Rose",
      "diamondCount": 1,
      "iconUrl": "https://example.invalid/rose.webp"
    }
  ]
}

Only enabled gifts with complete identity and price are returned; iconUrl may be null. key is the effect trigger identifier and is unique within one game; different games may reuse it. Build a providerGiftId → key map whenever the catalog is loaded or refreshed. Gift WebSocket events still contain only providerGiftId, not key. If an event's providerGiftId is absent from the catalog, do not guess or construct a key; safely skip the effect or use a game-defined generic fallback. Runtime event processing must still use the canonical payload and eventId received from Core. Use credentials.gameApi.token, not the game access Token, for /openapi/v2/*.

End the Session explicitly:

DELETE /api/game-client/v2/session
Authorization: Bearer <sessionAccessToken>

Success is 204 No Content. The active socket closes with 4002 CLIENT_LOGOUT; then erase the password, Tokens, and Tickets from memory.

#Release checklist

  • Validate every password login, refresh, Ticket, ready, event, and acknowledgement shape.
  • Validate independent-backend HTTP Bearer, nbt.auth, in-place renewal, expiry, and the trusted URL policy.
  • Keep secrets memory-only and refresh serially.
  • Reissue a Ticket and validate a new leaseGeneration after every reconnect.
  • Persist idempotency by eventId; never ACK a failed effect.
  • Batch ACKs at 25ms / 100 events, within the 500-ID hard limit.
  • Test Production disconnect recovery, Debug success and failure, slow-consumer handling, and sign out.
  • Distinguish the three reconnectable close codes from every terminal close code.
  • Use only platform public HTTPS/WSS hosts plus the build-configured trusted backend; redact credentials from logs and telemetry.
  • Use the Leaderboard API only with the current credentials.gameApi.token.