# NBTiktok Interactive Game Integration API v2

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

| Capability | Method / path | Authentication |
| --- | --- | --- |
| Sign in and create Session | `POST /api/game-client/v2/login` | Email, password, game, mode, and build only |
| Removed device authorization | `POST /api/game-client/v2/device-authorizations` and `/token` | Returns `410 DEVICE_AUTHORIZATION_REMOVED` |
| Removed CAPTCHA | `GET /api/game-client/v2/captcha` | Returns `410 CAPTCHA_LOGIN_REMOVED` |
| Inspect Session | `GET /api/game-client/v2/session` | `Bearer <sessionAccessToken>` |
| Rotate credentials | `POST /api/game-client/v2/session/refresh` | `Bearer <refreshToken>`; empty body |
| Issue WebSocket Ticket | `POST /api/game-client/v2/session/websocket-ticket` | `Bearer <sessionAccessToken>`; empty body |
| Sign out | `DELETE /api/game-client/v2/session` | `Bearer <sessionAccessToken>` |
| Event connection | `GET /ws/game-client/v2?ticket=...` (WebSocket Upgrade) | One-use 60-second Ticket |
| Public game discovery | `GET /api/games/v2` | None |
| Localized game guide | `GET /api/games/v2/:gameId/guide?locale=...` | None |
| Game gift catalog | `GET /api/games/v2/:gameId/gifts` | None |
| Game screenshot | `GET /api/games/v2/:gameId/screenshots/:position` | None; `position` is 1–5 |
| Leaderboards and player attributes | `/openapi/v2/*` | `Bearer <credentials.gameApi.token>` |
| Independent backend HTTP/WSS | Client-owned trusted URL | HTTP Bearer; WSS `nbt.auth` first frame |
| Backend signing keys | `GET /.well-known/game-backend-jwks.json` | None; 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:

```json
{ "error": "INVALID_CREDENTIALS" }
```

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

```json
{ "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

```http
POST /api/game-client/v2/login
Content-Type: application/json
```

```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

```json
{
  "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.

| HTTP | `error` | Required action |
| --- | --- | --- |
| 400 | `MODE_INVALID` | Correct `mode` |
| 400 | `CLIENT_BUILD_INVALID` | Send a safe integer of at least 1 |
| 401 | `INVALID_CREDENTIALS` | Email is unknown, password is wrong, or the account is temporarily locked; show one generic message |
| 403 | `DEALER_UNAVAILABLE` | Check dealer and membership state |
| 403 | `GAME_DISABLED` | Stop login for the disabled game |
| 403 | `GAME_NOT_AUTHORIZED` | Obtain a valid game authorization |
| 404 | `GAME_NOT_FOUND` | Correct `gameId` |
| 409 | `ROOM_ID_REQUIRED` | Add the live-room ID to the profile |
| 409 | `PROFILE_INCOMPLETE` | Complete the profile region |
| 409 | `GIFT_CREDIT_EXHAUSTED` | Restore the account's gift credit before login |
| 409 | `BILLING_CONFIG_REQUIRED` | Wait for Production billing configuration |
| 426 | `CLIENT_BUILD_UNSUPPORTED` | Upgrade to the returned `minimumClientBuild` |
| 429 | `LOGIN_RATE_LIMITED` / `RATE_LIMITED` | Honor `Retry-After` and back off |
| 503 | `AUTH_UNAVAILABLE` / `BILLING_BACKLOG_PAUSED` | Back 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:

```http
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:

```http
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:

```json
{ "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:

```text
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:

```http
POST /api/game-client/v2/session/websocket-ticket
Authorization: Bearer <sessionAccessToken>
```

```json
{
  "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:

```json
{
  "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.

```json
{
  "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.

```json
{
  "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:

```json
{
  "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.

```json
{
  "type": "ack",
  "gameId": "game_one",
  "leaseGeneration": 4,
  "eventIds": ["2d2a0ef5-2388-45de-a255-2dcc73543d3b"]
}
```

```json
{
  "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:

```json
{
  "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:

```json
{
  "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.

| Close | Example reason | Continue Session | Client action |
| --- | --- | --- | --- |
| `4001` | `SESSION_REPLACED` | No | Stop the old client and erase credentials |
| `4002` | `CLIENT_LOGOUT` | No | Normal sign-out completed |
| `4003` | `SESSION_INVALID` / `SESSION_EXPIRED` / `SESSION_REAUTH_REQUIRED` | No | Sign in again |
| `4004` | `AUTHORIZATION_INVALID` / `AUTHORIZATION_EXPIRED` | No | Repair authorization, then sign in |
| `4005` | `GAME_DELETED` / `GAME_DISABLED` | No | Stop the game and report its state |
| `4006` | `CLIENT_BUILD_UNSUPPORTED` | No | Upgrade the client |
| `4007` | `STREAM_ENDED` | No | End this Production run |
| `4008` | `SOURCE_BINDING_CHANGED` | No | Refresh profile authority, then sign in |
| `4009` | Administrative or release shutdown | No | Surface the reason and sign in again if appropriate |
| `4010` | `SLOW_CONSUMER` | Yes | Remove processing blockage, then reconnect with backoff |
| `4011` | `SOURCE_RELEASED` | Yes | Request a new Ticket after backoff |
| `4012` | `BALANCE_EXHAUSTED` | No | Erase credentials and restore credit |
| `4013` | `HEARTBEAT_TIMEOUT` | Yes | Check the network and reconnect with backoff |
| `4014` | `REFRESH_TOKEN_REUSED` | No | Erase 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:

```http
GET /api/games/v2/game_one/gifts
Accept: application/json
```

```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:

```http
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](./leaderboard.en.md) only with the current `credentials.gameApi.token`.
