v2 · 开发者指南
DOC / 01交互游戏接入 API
邮箱密码登录、客户端 Session、WebSocket 标准事件、可靠 ACK 与 Debug 调试。
本 API 供游戏客户端以 Email 与密码建立 Session、按需认证独立游戏业务后台、通过 WebSocket 接收直播交互事件,以及在 Debug 模式测试相同流程。
请只使用 NBTiktok 提供的公开 HTTPS Origin;不得直接连接容器、诊断端口、/internal/* 或管理路由。
#接入流程
- 调用
POST /api/game-client/v2/login,提交 Email、密码、gameId、mode与clientBuild;不使用 CAPTCHA。 - 直接接入排行榜或玩家属性时不要发送
backend,响应提供credentials.gameApi。 - 启用独立后台时,登录响应返回
backendAuth与credentials.gameBackend;URL 由客户端可信配置选择。 credentials.gameBackend只能传到目前 build 选定的可信 URL。- 使用一次性 WebSocket Ticket 连接并验证
ready;Production 持久化后 ACK,Debug 处理并确认debug_command。 - 在
refreshAt轮换整组凭证并续签第三方 WSS;平台事件连接断线时申请新 Ticket,最后以DELETE /api/game-client/v2/session结束 Session。
#公开路由
| 能力 | Method / Path | 验证 |
|---|---|---|
| 登录并建立 Session | POST /api/game-client/v2/login | 仅 Email、密码、游戏、模式与 build |
| 已移除的设备授权 | POST /api/game-client/v2/device-authorizations 与 /token | 返回 410 DEVICE_AUTHORIZATION_REMOVED |
| 已移除的 CAPTCHA | GET /api/game-client/v2/captcha | 返回 410 CAPTCHA_LOGIN_REMOVED |
| 查询 Session | GET /api/game-client/v2/session | Bearer <sessionAccessToken> |
| 轮换凭证 | POST /api/game-client/v2/session/refresh | Bearer <refreshToken>;空请求体 |
| 申请 WebSocket Ticket | POST /api/game-client/v2/session/websocket-ticket | Bearer <sessionAccessToken>;空请求体 |
| 登出 | DELETE /api/game-client/v2/session | Bearer <sessionAccessToken> |
| 事件连接 | GET /ws/game-client/v2?ticket=... | 60 秒、仅可使用一次的 Ticket |
| 浏览公开游戏 | GET /api/games/v2 | 无 |
| 查询本地化玩法指南 | GET /api/games/v2/:gameId/guide?locale=... | 无 |
| 游戏礼物目录 | GET /api/games/v2/:gameId/gifts | 无 |
| 游戏截图 | GET /api/games/v2/:gameId/screenshots/:position | 无;position 为 1~5 |
| 排行榜与玩家属性 | /openapi/v2/* | Bearer <credentials.gameApi.token> |
| 独立游戏后台 HTTP/WSS | 客户端可信 URL | HTTP Bearer;WSS nbt.auth 首帧 |
| 后台验签公钥 | GET /.well-known/game-backend-jwks.json | 无;依 Cache-Control 缓存 |
除 204 与成功的截图二进制响应外,HTTP 响应体均为 JSON。登录使用扁平格式 { "error": "INVALID_CREDENTIALS" };Session、WebSocket Ticket、/openapi/v2/* 与网关限流使用 { "error": { "code": "ERROR_CODE", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }。
#Email 密码登录
POST /api/game-client/v2/login
Content-Type: application/json
{
"email": "streamer@example.com",
"password": "account-password",
"gameId": "game_one",
"mode": "production",
"clientBuild": 120
}
密码只提交给 NBTiktok 平台,请求结束后立即从内存清除,绝不可写入配置、日志、URL、崩溃报告或遥测。平台永远不会把 Email、密码、登录 Token 或 Cookie 传给独立后台。Email 密码决定账号及其 Sandbox 归属。
#登录成功响应
{
"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
}
}
服务器返回的 ID、房间、模式、版本策略、期限与路径均为权威数据。启用独立后台时 backendAuth 为对象且提供 credentials.gameBackend;未启用时 backendAuth 为 null 且省略该凭证。密码、Token 与 Ticket 不得写入 PlayerPrefs、配置文件、日志、URL、崩溃报告或遥测。
session access Token 最长 30 分钟,Game API Token 最长 10 分钟,独立后台 JWT 最长 30 分钟。由于 refreshAt 取所有短期凭证刷新点的最早值,整组凭证通常仍在签发后约 8 分钟 refresh。refresh Token 仅可使用一次,Session 滑动期限最长 24 小时,absoluteExpiresAt 最晚为登录后 7 天;Ticket 最长 60 秒且仅可成功消耗一次。
同账号同时只允许一个 Production Session,新 Production 登录即使是不同游戏也会撤销旧 Session。Debug 依 authorizationId 保留一个 Session,可与 Production 同时存在;同一授权的新 Debug 登录会替换旧 Session。
#登录与 Session 建立错误
| HTTP | error | 处理 |
|---|---|---|
| 400 | MODE_INVALID | 修正 mode |
| 400 | CLIENT_BUILD_INVALID | 提交至少为 1 的安全整数 |
| 401 | INVALID_CREDENTIALS | Email 不存在、密码错误或账号暂时锁定;显示一致消息 |
| 403 | DEALER_UNAVAILABLE | 检查经销商与成员状态 |
| 403 | GAME_DISABLED | 停止登录已禁用游戏 |
| 403 | GAME_NOT_AUTHORIZED | 获取有效游戏授权 |
| 404 | GAME_NOT_FOUND | 修正 gameId |
| 409 | ROOM_ID_REQUIRED | 在个人信息填写直播间 ID |
| 409 | PROFILE_INCOMPLETE | 补齐地区信息 |
| 409 | GIFT_CREDIT_EXHAUSTED | 恢复礼物额度后再登录 |
| 409 | BILLING_CONFIG_REQUIRED | 等待 Production 计费配置完成 |
| 426 | CLIENT_BUILD_UNSUPPORTED | 升级到响应的 minimumClientBuild |
| 429 | LOGIN_RATE_LIMITED / RATE_LIMITED | 遵循 Retry-After 并退避 |
| 503 | AUTH_UNAVAILABLE / BILLING_BACKLOG_PAUSED | 指数退避并提示用户 |
426 登录错误仍使用扁平响应:{ "error": "CLIENT_BUILD_UNSUPPORTED" };客户端升级到当前 minimumClientBuild 后再登录。
#独立后台路由
NBTiktok 不保存也不返回业务 API URL。本地、测试与正式版本使用相同的 credentials.gameBackend JWT、精确的游戏 Audience 与 JWKS 验证。客户端从可信 build 配置读取 URL;开发 build 可在本地覆盖。登录请求不传 URL 或额外 Secret。
响应仅包含 backendAuth 的 Audience、运营方、隐私政策与配置版本,不含路由。JWT 只能传给客户端选定的可信 Origin,并拒绝跨 Origin 重定向。
HTTP 使用 Authorization: Bearer 发送该 JWT。第三方独立后台 WebSocket 使用固定子协议 nbt.game-backend.v1,连接后 5 秒内只在首个 {"type":"nbt.auth","token":"..."} 文本帧发送同一 JWT,绝不写入 URL、Cookie 或子协议。平台 Session refresh 返回新 Backend JWT 后,在原连接再次发送同一认证帧完成续签;若不再返回 gameBackend,立即关闭第三方连接。
第三方后台 WSS 与平台 /ws/game-client/v2 完全独立:前者使用 Backend JWT,后者继续使用一次性 credentials.websocket.ticket。可信构建分别配置 NBT_GAME_BACKEND_HTTP_URL 和 NBT_GAME_BACKEND_WS_URL;明文 http/ws 仅允许 localhost、127.0.0.0/8、[::1],其他地址强制 https/wss。
浏览器 Portal 使用 NBTiktok Email 密码重新验证及 Authorization Code + PKCE。正式回调精确匹配已登记 HTTPS URI;本地回调只允许 127.0.0.1 或 [::1] 的 /__nbt/callback。
#已退役端点
POST /api/game-client/v2/device-authorizations、POST /api/game-client/v2/device-authorizations/token 与 GET /api/game-client/v2/captcha 已退役,分别固定返回 410 DEVICE_AUTHORIZATION_REMOVED 或 410 CAPTCHA_LOGIN_REMOVED。新客户端只能使用 Email 密码登录,不得回退到设备码或 CAPTCHA 流程。
#Session 与凭证轮换
GET /api/game-client/v2/session
Authorization: Bearer <sessionAccessToken>
所有 Session 都保留 credentialSource="user_session" 与 credentialId=null。请核对身份、模式、直播间、build 与到期字段;后台路由仍由客户端配置持有。Sandbox 的 roomId 可为 null。
POST /api/game-client/v2/session/refresh
Authorization: Bearer <refreshToken>
空请求体成功响应会提供新的 session、权威 backendAuth、credentials.sessionAccess、credentials.gameApi、可选的 credentials.gameBackend 与一次性 credentials.refresh,但不含 websocket。整组凭证需原子替换,停止使用旧 Token 发起 HTTP,以新 Backend JWT 在原第三方 WSS 连接发送 nbt.auth 续签,并丢弃未使用的平台 Ticket。若不再返回 gameBackend,立即关闭第三方连接并停止调用独立后台。
重复使用旧 refresh Token 会返回 REFRESH_TOKEN_REUSED,撤销整个 Session 并以 4014 关闭 WebSocket。未知或过期 Token 返回 401 REFRESH_TOKEN_INVALID;Session 绝对期限或上下文失效时返回 401 SESSION_REAUTH_REQUIRED。若网络错误导致无法判定是否已消耗,不得重送同一 Token;应清除内存凭证并重新登录。refreshAt 永远是 ISO 8601 时间;若客户端恢复时已到期,应立即执行一次串行 refresh;收到 SESSION_REAUTH_REQUIRED 时清除凭证并重新登录。
#WebSocket 连接与围栏
将公开 HTTPS Origin 转成 wss://,再附加相对路径:
wss://<platform-origin>/ws/game-client/v2?ticket=<single-use-ticket>
URL 只能放 Ticket。Ticket 无效、已用或过期时握手为 401;计费状态可能返回 409;容量不足或直播源异常可能返回 503。断线后,以仍有效的 access Token 调用 POST /api/game-client/v2/session/websocket-ticket 获取新 Ticket。
第一个业务消息必须是:
{
"type": "ready",
"sessionId": "6fe3890b-bb7c-4e45-b2b2-1db2d77751ce",
"userId": "cf8cbe61-95c3-45f8-a883-2555e99d408f",
"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
}
逐项核对 sessionId、streamerId、authorizationId、gameId、mode、roomId、clientBuild 与 minimumClientBuild,并要求 userId 是 UUID、leaseGeneration 是正整数。任何不符都拒绝连接。所有事件与 ACK 队列要绑定 gameId 与 leaseGeneration;不得复用旧连接状态不得重用。心跳使用 WebSocket 协议 Ping/Pong,不要传送自定义 { "type": "pong" }。
#Production 事件与可靠 ACK
ready 后拉取未确认事件;limit 默认 100、最大 200:
{
"type": "pull",
"gameId": "game_one",
"leaseGeneration": 4,
"limit": 100
}
事件 envelope:
{
"type": "event",
"gameId": "game_one",
"leaseGeneration": 4,
"event": {
"eventId": "2d2a0ef5-2388-45de-a255-2dcc73543d3b",
"eventType": "gift",
"occurredAt": "2026-08-19T12:00:00.000Z",
"payload": {
"player": { "sourcePlayerId": "viewer-id", "displayName": "Viewer" },
"gift": {
"providerGiftId": "5655",
"displayName": "Rose",
"totalCount": 1,
"deltaCount": 1,
"unitDiamondCount": 5,
"giftCoins": 5,
"missingPrice": false,
"groupId": "gift-group",
"repeatEnd": true
}
}
}
}
eventId 是 UUID,occurredAt 是 ISO 8601;gameId 与 leaseGeneration 仅存在外层 envelope。eventType 包含 comment、like、gift、stream_end。留言使用 payload.player 与 content;点赞使用 payload.player 与 count;礼物使用上述嵌套的 player、gift。
[
{
"eventId": "b59fc69c-4c6a-4c09-990b-e0d6514b03fd",
"eventType": "comment",
"occurredAt": "2026-08-19T12:00:01.123Z",
"payload": { "player": { "sourcePlayerId": "viewer-id", "displayName": "Viewer" }, "content": "join" }
},
{
"eventId": "3a83d2f9-2095-4821-926d-ff8a80a0e771",
"eventType": "like",
"occurredAt": "2026-08-19T12:00:02.000Z",
"payload": { "player": { "sourcePlayerId": "viewer-id", "displayName": "Viewer" }, "count": 12 }
}
]
礼物连击的 totalCount 是累计、deltaCount 是本事件增量;游戏发奖应使用 deltaCount。groupId 可为 null,repeatEnd 表示连击结束。unitDiamondCount 可为 0 且 missingPrice: true;游戏不得从事件推断平台扣费,也不能把 ack_result.consumed 当成钻石数。
只有 comment、gift 持久化重播并需要 ACK;like 仅实时、不 ACK;stream_end 不重播、不 ACK,之后以 4007 关闭。新增的未知字段应忽略,但必须以权威 eventId 做幂等。
ACK 表示效果与处理状态都已可靠保存,而非只收到 WebSocket。失败事件不得 ACK,让 Core 重播;已完成的重播不得重做效果,只重新 ACK 同一 eventId。
{
"type": "ack",
"gameId": "game_one",
"leaseGeneration": 4,
"eventIds": ["2d2a0ef5-2388-45de-a255-2dcc73543d3b"]
}
单批硬上限为 500 个不重复 UUID;建议 25ms 或累计 100 个事件即送出,以先满足者为准,且同一连接只保留一个在途批次。
{
"type": "ack_result",
"gameId": "game_one",
"leaseGeneration": 4,
"acknowledged": 1,
"consumed": 1,
"duplicate": 0
}
acknowledged 是首次确认数,consumed 是首次进入礼物消费审计的事件数(不是钻石),duplicate 是先前已确认数。必须满足 acknowledged + duplicate === eventIds.length 且 consumed <= acknowledged;整批重播可为 acknowledged = 0, duplicate = N。无法匹配或计数错误代表协议失步:关闭连接、清空内存 ACK 队列、申请新 Ticket,再以重播与本地状态恢复。
#Debug 协议
Debug 的登录、refresh、Ticket、ready 与围栏相同。debug_command 包含 commandId、requiresAck: true、transient: true,以及具有 eventId、eventType、occurredAt、payload 的完整 event。指令不持久化、不重播、不进入 Production 事件、消费审计或扣费,通常应在 15 秒内响应。
{
"type": "debug_command",
"commandId": "da6c668b-9e00-48d0-8113-97fbd52dc43f",
"gameId": "game_one",
"leaseGeneration": 4,
"requiresAck": true,
"transient": true,
"event": {
"eventId": "75c93fa7-3e1e-4fbc-b736-37b21c49e3d6",
"eventType": "gift",
"occurredAt": "2026-08-19T12:00:03.000Z",
"payload": {}
}
}
执行后必须明确回复:
{
"type": "debug_command_ack",
"gameId": "game_one",
"leaseGeneration": 4,
"commandId": "79ac82bb-a842-4817-aeb8-cf345db37639",
"outcome": "processed"
}
失败时使用 "outcome": "failed",并提供 1–128 字符的 errorCode,例如 EFFECT_REJECTED;成功时必须省略 errorCode。Core 响应 type: "debug_command_ack_received",status 为 processed 或 failed。Debug 客户端不得发送 Production pull 或 ack。
#错误、礼物目录与登出
协议错误格式是 { "type": "error", "error": "ERROR_CODE" }。SESSION_FENCE_STALE、ACK_EVENT_INVALID、ACK_FAILED 或 Debug 围栏错误都应断线并由持久化状态恢复。
| 关闭码 | 原因 | 是否续用 Session | 行为 |
|---|---|---|---|
4001 | SESSION_REPLACED | 否 | 停止旧客户端并清除凭证 |
4002 | CLIENT_LOGOUT | 否 | 正常退出 |
4003 | SESSION_INVALID / SESSION_EXPIRED / SESSION_REAUTH_REQUIRED | 否 | 重新登录 |
4004 | AUTHORIZATION_INVALID / AUTHORIZATION_EXPIRED | 否 | 修复授权后登录 |
4005 | GAME_DELETED / GAME_DISABLED | 否 | 停止游戏 |
4006 | CLIENT_BUILD_UNSUPPORTED | 否 | 升级客户端 |
4007 | STREAM_ENDED | 否 | 结束本场 Production |
4008 | SOURCE_BINDING_CHANGED | 否 | 更新个人信息后登录 |
4009 | 管理或发布下线 | 否 | 显示原因并视情况重新登录 |
4010 | SLOW_CONSUMER | 是 | 降低阻塞,退避重连 |
4011 | SOURCE_RELEASED | 是 | 退避后申请新 Ticket |
4012 | BALANCE_EXHAUSTED | 否 | 清除凭证并补充额度 |
4013 | HEARTBEAT_TIMEOUT | 是 | 检查网络后退避重连 |
4014 | REFRESH_TOKEN_REUSED | 否 | 清除全部凭证再登录 |
只有 4010、4011、4013 可用有上限的指数退避加随机抖动重连;其他均为 Session 终止。
礼物目录使用 GET /api/games/v2/game_one/gifts,无需验证。响应包含 game.gameId、game.displayName,每个有效礼物含必填 key、catalogGiftId、providerGiftId、displayName、diamondCount、可为 null 的 iconUrl。key 是当前游戏内唯一的效果触发识别,不同游戏可重复使用;加载或更新目录时应建立 providerGiftId → key 对照。WebSocket 礼物事件本身仍只带 providerGiftId,不带 key;若事件的 providerGiftId 不在目录中,不得猜测或拼接 key,应安全跳过效果或使用游戏定义的通用降级效果。仅返回已启用且身份、价格完整的礼物;Production canonical gift event 仍是执行时事实来源。
结束时调用:
DELETE /api/game-client/v2/session
Authorization: Bearer <sessionAccessToken>
成功为 204 No Content,WebSocket 以 4002 CLIENT_LOGOUT 关闭;随后清除密码、Token 与 Ticket。排行榜必须使用独立 credentials.gameApi.token,不能使用 Session access Token;完整协议请参阅开放积分榜 API。
#上线检查清单
- 仅连接平台公开 HTTPS/WSS 与构建配置中的可信独立后台,不访问
/internal/*; - 验证密码登录、refresh、Ticket、
ready和事件结构; - 验证独立后台 HTTP Bearer、
nbt.auth、原连接续签、到期与可信 URL 规则; - 凭证只在内存,日志与遥测脱敏;
- refresh 串行执行并处理不确定网络结果;
- 每次重连获取新 Ticket、更新
leaseGeneration; comment、gift以持久化eventId幂等,失败不得 ACK;- ACK 使用 25ms / 100 条,单连接只有一个在途批次;
- Debug 区分
debug_command与 Productionevent; - 区分三个可重连码与所有终止码;
- 正常退出时调用 DELETE 并清除内存凭证。