v2 · Hướng dẫn dành cho nhà phát triển
DOC / 01API tích hợp trò chơi tương tác
Đăng nhập email/mật khẩu, phiên máy khách, sự kiện WebSocket, ACK tin cậy và kiểm thử Debug.
API này cho phép máy khách đăng nhập bằng email và mật khẩu, duy trì Session, tùy chọn xác thực với backend trò chơi triển khai độc lập, nhận sự kiện WebSocket và kiểm thử trong chế độ Debug.
Chỉ sử dụng HTTPS Origin công khai do NBTiktok cung cấp. Không kết nối trực tiếp tới cổng container, cổng chẩn đoán, /internal/* hoặc tuyến quản trị.
#Quy trình tích hợp
- Gọi
POST /api/game-client/v2/loginvới email, mật khẩu,gameId,modevàclientBuild; không dùng CAPTCHA. - Với tích hợp trực tiếp bảng xếp hạng hoặc thuộc tính người chơi, không gửi
backendvà dùngcredentials.gameApi. - Khi backend độc lập được bật, đăng nhập trả về
backendAuthvàcredentials.gameBackend; URL do cấu hình đáng tin cậy của máy khách chọn. - Chỉ gửi
credentials.gameBackendtới URL đáng tin cậy do build máy khách chọn. - Kết nối bằng WebSocket Ticket dùng một lần và kiểm tra
ready; Production lưu bền vững trước ACK, Debug phản hồidebug_command. - Luân chuyển toàn bộ credential và gia hạn WSS backend độc lập tại
refreshAt; xin Ticket nền tảng mới khi ngắt kết nối và kết thúc bằngDELETE /api/game-client/v2/session.
#Các tuyến công khai
| Khả năng | Method / Path | Xác thực |
|---|---|---|
| Đăng nhập và tạo Session | POST /api/game-client/v2/login | Chỉ email, mật khẩu, trò chơi, mode và build |
| Ủy quyền thiết bị đã xóa | POST /api/game-client/v2/device-authorizations và /token | Trả 410 DEVICE_AUTHORIZATION_REMOVED |
| CAPTCHA đã xóa | GET /api/game-client/v2/captcha | Trả 410 CAPTCHA_LOGIN_REMOVED |
| Xem Session | GET /api/game-client/v2/session | Bearer <sessionAccessToken> |
| Luân chuyển thông tin xác thực | POST /api/game-client/v2/session/refresh | Bearer <refreshToken>; body rỗng |
| Cấp WebSocket Ticket | POST /api/game-client/v2/session/websocket-ticket | Bearer <sessionAccessToken>; body rỗng |
| Đăng xuất | DELETE /api/game-client/v2/session | Bearer <sessionAccessToken> |
| Kết nối sự kiện | GET /ws/game-client/v2?ticket=... | Ticket dùng một lần, hiệu lực 60 giây |
| Khám phá game công khai | GET /api/games/v2 | Không |
| Hướng dẫn game theo locale | GET /api/games/v2/:gameId/guide?locale=... | Không |
| Danh mục quà tặng | GET /api/games/v2/:gameId/gifts | Không |
| Ảnh chụp trò chơi | GET /api/games/v2/:gameId/screenshots/:position | Không; position từ 1–5 |
| Bảng xếp hạng và thuộc tính người chơi | /openapi/v2/* | Bearer <credentials.gameApi.token> |
| HTTP/WSS backend độc lập | URL đáng tin cậy của máy khách | HTTP Bearer; frame đầu WSS nbt.auth |
| Khóa xác minh backend | GET /.well-known/game-backend-jwks.json | Không; cache theo Cache-Control |
Ngoại trừ 204 và ảnh nhị phân, body HTTP là JSON. Đăng nhập dùng envelope phẳng { "error": "INVALID_CREDENTIALS" }. Session, WebSocket Ticket, /openapi/v2/* và giới hạn gateway dùng { "error": { "code": "ERROR_CODE", "requestId": "018f1f6d-7b2a-7e34-a6cc-5e5f3fb70420" } }.
#Đăng nhập bằng email và mật khẩu
POST /api/game-client/v2/login
Content-Type: application/json
{
"email": "streamer@example.com",
"password": "account-password",
"gameId": "game_one",
"mode": "production",
"clientBuild": 120
}
Mật khẩu chỉ được gửi tới nền tảng NBTiktok và phải xóa khỏi bộ nhớ sau request. Không lưu trong PlayerPrefs, cấu hình, log, URL, báo cáo lỗi hay telemetry. Nền tảng không bao giờ đặt email, mật khẩu, Token đăng nhập hoặc Cookie vào JWT backend hay gửi chúng cho backend độc lập. Email và mật khẩu chọn tài khoản cùng Sandbox riêng của tài khoản.
#Phản hồi đăng nhập thành công
{
"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
}
}
Các ID, phòng, chế độ, chính sách bản dựng, thời hạn và đường dẫn trong phản hồi là dữ liệu có thẩm quyền. Không ghi mật khẩu, Token hay Ticket vào PlayerPrefs, tệp cấu hình, log, URL, báo cáo lỗi hoặc telemetry.
Session access Token tối đa 30 phút, Game API Token tối đa 10 phút và JWT backend độc lập tối đa 30 phút. Vì refreshAt là điểm sớm nhất của mọi credential ngắn hạn, cả bộ thường vẫn refresh khoảng tám phút sau khi phát hành. Refresh Token chỉ dùng một lần, Session trượt tối đa 24 giờ; Ticket tối đa 60 giây và chỉ tiêu thụ một lần.
Mỗi tài khoản chỉ có một Production Session; đăng nhập Production mới thu hồi Session cũ kể cả khi là trò chơi khác. Debug giữ một Session theo authorizationId, có thể chạy cùng Production; đăng nhập Debug mới cho cùng quyền thay Session Debug cũ.
#Lỗi đăng nhập và tạo Session
| HTTP | error | Xử lý |
|---|---|---|
| 400 | MODE_INVALID | Sửa mode |
| 400 | CLIENT_BUILD_INVALID | Gửi số nguyên an toàn từ 1 |
| 401 | INVALID_CREDENTIALS | Email không tồn tại, mật khẩu sai hoặc tài khoản tạm khóa; hiển thị cùng một thông báo |
| 403 | DEALER_UNAVAILABLE | Kiểm tra đại lý và tư cách thành viên |
| 403 | GAME_DISABLED | Dừng đăng nhập trò chơi bị tắt |
| 403 | GAME_NOT_AUTHORIZED | Cấp quyền trò chơi hợp lệ |
| 404 | GAME_NOT_FOUND | Sửa gameId |
| 409 | ROOM_ID_REQUIRED | Thêm ID phòng livestream vào hồ sơ |
| 409 | PROFILE_INCOMPLETE | Hoàn thiện vùng trong hồ sơ |
| 409 | GIFT_CREDIT_EXHAUSTED | Khôi phục hạn mức quà rồi đăng nhập |
| 409 | BILLING_CONFIG_REQUIRED | Chờ cấu hình thanh toán Production |
| 426 | CLIENT_BUILD_UNSUPPORTED | Nâng lên minimumClientBuild trả về |
| 429 | LOGIN_RATE_LIMITED / RATE_LIMITED | Tuân theo Retry-After và backoff |
| 503 | AUTH_UNAVAILABLE / BILLING_BACKLOG_PAUSED | Backoff lũy thừa và báo người dùng |
Phản hồi 426 dùng envelope V2: error.code là CLIENT_BUILD_UNSUPPORTED, error.requestId là ID tương quan, và error.details chứa clientBuild đã gửi cùng minimumClientBuild hiện tại.
#Định tuyến backend độc lập
NBTiktok không lưu hoặc trả về URL API nghiệp vụ. Bản cục bộ, kiểm thử và chính thức dùng cùng JWT credentials.gameBackend, audience chính xác theo trò chơi và cách xác minh JWKS. Máy khách đọc URL từ cấu hình build đáng tin cậy; build phát triển có thể ghi đè cục bộ. Request đăng nhập không gửi URL hay Secret bổ sung.
Phản hồi chỉ có backendAuth gồm audience, nhà vận hành, chính sách riêng tư và phiên bản cấu hình, không có route. Chỉ gửi JWT tới Origin đáng tin cậy do máy khách chọn và từ chối redirect khác Origin.
HTTP gửi JWT bằng Authorization: Bearer. WebSocket của backend độc lập dùng subprotocol cố định nbt.game-backend.v1 và chỉ gửi cùng JWT trong text frame đầu tiên {"type":"nbt.auth","token":"..."}, không bao giờ trong URL. Sau refresh Session, gửi lại frame đó với JWT mới trên kết nối hiện tại. Đây không phải /ws/game-client/v2 của nền tảng; endpoint nền tảng tiếp tục dùng credentials.websocket.ticket một lần.
Build đáng tin cậy nên cấu hình riêng NBT_GAME_BACKEND_HTTP_URL và NBT_GAME_BACKEND_WS_URL. Chỉ localhost, 127.0.0.0/8 và [::1] được dùng http/ws rõ; địa chỉ từ xa phải dùng https/wss.
Portal dùng xác thực lại email/mật khẩu NBTiktok cùng Authorization Code và PKCE. Callback chính thức khớp chính xác URI HTTPS đã đăng ký; callback cục bộ chỉ dùng 127.0.0.1 hoặc [::1] với đường dẫn /__nbt/callback.
#Endpoint đã ngừng hoạt động
#Session và luân chuyển thông tin xác thực
GET /api/game-client/v2/session
Authorization: Bearer <sessionAccessToken>
Mọi Session giữ các trường tương thích credentialSource="user_session" và credentialId=null. Kiểm tra danh tính, mode, room, build và thời hạn; cấu hình định tuyến backend vẫn thuộc máy khách. roomId của Sandbox có thể là null.
POST /api/game-client/v2/session/refresh
Authorization: Bearer <refreshToken>
Phản hồi body rỗng trả Session, backendAuth có thẩm quyền, access, Game API, game backend tùy chọn và refresh Token mới; không trả websocket. Cài đặt cả bộ mới theo cách nguyên tử, ngừng dùng Token HTTP cũ, gia hạn WebSocket backend độc lập bằng Backend JWT mới và bỏ Ticket nền tảng chưa dùng. Nếu không còn gameBackend, đóng kết nối backend độc lập và ngừng gọi nó.
Refresh Token đã dùng trả REFRESH_TOKEN_REUSED, thu hồi Session và đóng socket với 4014. Token không biết hoặc hết hạn trả 401 REFRESH_TOKEN_INVALID; Session hết hạn tuyệt đối hoặc sai ngữ cảnh trả 401 SESSION_REAUTH_REQUIRED. Nếu lỗi mạng làm trạng thái tiêu thụ không rõ ràng, không gửi lại Token đó; hãy xóa thông tin trong bộ nhớ và đăng nhập lại. refreshAt luôn là thời điểm ISO 8601; nếu đã đến hạn khi client hoạt động lại, hãy thực hiện ngay một refresh tuần tự. Khi nhận SESSION_REAUTH_REQUIRED, xóa credential và đăng nhập lại.
#Kết nối WebSocket và hàng rào
Đổi HTTPS Origin công khai thành wss://, rồi nối đường dẫn tương đối:
wss://<platform-origin>/ws/game-client/v2?ticket=<single-use-ticket>
URL chỉ được chứa Ticket. Ticket sai, đã dùng hoặc hết hạn nhận 401; trạng thái thanh toán có thể nhận 409; thiếu dung lượng hoặc nguồn trực tiếp có thể nhận 503. Sau khi ngắt kết nối, gọi POST /api/game-client/v2/session/websocket-ticket bằng access Token còn hạn.
Thông điệp nghiệp vụ đầu tiên phải là:
{
"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
}
So sánh sessionId, streamerId, authorizationId, gameId, mode, roomId, clientBuild, minimumClientBuild; yêu cầu userId là UUID và leaseGeneration là số nguyên dương. Từ chối nếu khác thẩm quyền cục bộ. Gắn hàng đợi sự kiện và ACK với gameId cùng leaseGeneration; không tái sử dụng trạng thái kết nối cũ. Nhịp tim dùng Ping/Pong của giao thức WebSocket, không gửi JSON { "type": "pong" } tùy chỉnh.
#Sự kiện Production và ACK tin cậy
Sau ready, kéo sự kiện tin cậy chưa xác nhận. limit mặc định 100 và tối đa 200.
{
"type": "pull",
"gameId": "game_one",
"leaseGeneration": 4,
"limit": 100
}
{
"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 là UUID, occurredAt là ISO 8601; gameId và leaseGeneration chỉ ở envelope ngoài. eventType gồm comment, like, gift, stream_end. Bình luận dùng payload.player cùng content; lượt thích dùng payload.player cùng count; quà dùng payload.player và đối tượng payload.gift ở trên.
[
{
"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 }
}
]
Trong chuỗi quà, totalCount là tổng tích lũy còn deltaCount là phần tăng của sự kiện; cấp hiệu ứng theo deltaCount. groupId có thể null, repeatEnd báo kết thúc chuỗi. unitDiamondCount có thể bằng 0 với missingPrice: true; trò chơi không được suy ra thanh toán nền tảng và không coi ack_result.consumed là số kim cương.
Chỉ comment và gift được lưu để phát lại và cần ACK. like chỉ thời gian thực, không ACK. stream_end không phát lại hoặc ACK và sau đó đóng bằng 4007. Bỏ qua trường mới, nhưng luôn dùng eventId chuẩn để chống lặp.
ACK có nghĩa là hiệu ứng và bản ghi xử lý đã được lưu bền vững, không chỉ là đã nhận WebSocket. Không ACK sự kiện thất bại để Core phát lại. Nếu sự kiện phát lại đã hoàn tất cục bộ, bỏ qua hiệu ứng và ACK lại cùng eventId.
{
"type": "ack",
"gameId": "game_one",
"leaseGeneration": 4,
"eventIds": ["2d2a0ef5-2388-45de-a255-2dcc73543d3b"]
}
Mỗi lô tối đa 500 UUID không trùng. Khuyến nghị gửi sau 25ms hoặc đủ 100 sự kiện, điều kiện nào đến trước; mỗi kết nối chỉ có một lô đang chờ.
{
"type": "ack_result",
"gameId": "game_one",
"leaseGeneration": 4,
"acknowledged": 1,
"consumed": 1,
"duplicate": 0
}
acknowledged là số xác nhận lần đầu, consumed là số sự kiện lần đầu vào kiểm toán tiêu thụ quà (không phải kim cương), duplicate là số đã xác nhận trước. Phải thỏa acknowledged + duplicate === số eventIds đã gửi và consumed <= acknowledged; lô phát lại toàn bộ có thể trả acknowledged = 0, duplicate = N. Kết quả sai là lệch giao thức: đóng kết nối, xóa hàng đợi ACK, xin Ticket mới và khôi phục từ phát lại.
#Giao thức Debug
Debug dùng cùng đăng nhập, refresh, Ticket, ready và hàng rào. debug_command chứa commandId, requiresAck: true, transient: true và event đầy đủ với eventId, eventType, occurredAt, payload. Lệnh không lưu, không phát lại, không vào sự kiện Production, kiểm toán hay thanh toán; thường phải trả trong 15 giây.
{
"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": {}
}
}
Luôn phản hồi kết quả:
{
"type": "debug_command_ack",
"gameId": "game_one",
"leaseGeneration": 4,
"commandId": "79ac82bb-a842-4817-aeb8-cf345db37639",
"outcome": "processed"
}
Khi thất bại, dùng "outcome": "failed" và errorCode dài 1–128 ký tự, ví dụ EFFECT_REJECTED; khi thành công phải bỏ errorCode. Core xác nhận bằng type: "debug_command_ack_received", status là processed hoặc failed. Máy khách Debug không được gửi Production pull hay ack.
#Lỗi, danh mục quà và đăng xuất
Lỗi giao thức có dạng { "type": "error", "error": "ERROR_CODE" }. Với SESSION_FENCE_STALE, ACK_EVENT_INVALID, ACK_FAILED hoặc lỗi hàng rào Debug, hãy ngắt kết nối và khôi phục từ trạng thái bền vững.
| Mã đóng | Lý do | Tiếp tục Session | Hành động |
|---|---|---|---|
4001 | SESSION_REPLACED | Không | Dừng máy khách cũ, xóa thông tin |
4002 | CLIENT_LOGOUT | Không | Đăng xuất bình thường |
4003 | SESSION_INVALID / SESSION_EXPIRED / SESSION_REAUTH_REQUIRED | Không | Đăng nhập lại |
4004 | AUTHORIZATION_INVALID / AUTHORIZATION_EXPIRED | Không | Sửa quyền rồi đăng nhập |
4005 | GAME_DELETED / GAME_DISABLED | Không | Dừng trò chơi |
4006 | CLIENT_BUILD_UNSUPPORTED | Không | Nâng cấp máy khách |
4007 | STREAM_ENDED | Không | Kết thúc lượt Production |
4008 | SOURCE_BINDING_CHANGED | Không | Cập nhật hồ sơ rồi đăng nhập |
4009 | Quản trị hoặc phát hành dừng | Không | Hiện reason và xử lý phù hợp |
4010 | SLOW_CONSUMER | Có | Giảm nghẽn, backoff rồi kết nối |
4011 | SOURCE_RELEASED | Có | Backoff rồi xin Ticket mới |
4012 | BALANCE_EXHAUSTED | Không | Xóa thông tin và bổ sung hạn mức |
4013 | HEARTBEAT_TIMEOUT | Có | Kiểm tra mạng rồi backoff |
4014 | REFRESH_TOKEN_REUSED | Không | Xóa toàn bộ và đăng nhập lại |
Chỉ 4010, 4011, 4013 được kết nối lại bằng backoff lũy thừa có jitter và giới hạn; các mã khác kết thúc Session.
Danh mục công khai GET /api/games/v2/game_one/gifts không cần xác thực. Phản hồi có game.gameId, game.displayName; mỗi quà hợp lệ có key bắt buộc, catalogGiftId, providerGiftId, displayName, diamondCount, và iconUrl có thể null. key xác định hiệu ứng và chỉ duy nhất trong một game; các game khác nhau có thể dùng lại cùng key. Khi tải hoặc làm mới danh mục, hãy tạo ánh xạ providerGiftId → key. Sự kiện quà WebSocket vẫn chỉ chứa providerGiftId, không chứa key; nếu ID sự kiện không có trong danh mục, không được đoán hay tự ghép key, mà phải bỏ qua hiệu ứng an toàn hoặc dùng hiệu ứng dự phòng chung do game định nghĩa. Chỉ trả quà đang bật, đủ danh tính và giá; canonical gift event vẫn là sự thật khi chạy.
Đăng xuất bằng:
DELETE /api/game-client/v2/session
Authorization: Bearer <sessionAccessToken>
Thành công trả 204 No Content; WebSocket đóng với 4002 CLIENT_LOGOUT. Sau đó xóa mã thiết bị, Token và Ticket khỏi bộ nhớ. Bảng xếp hạng phải dùng credentials.gameApi.token riêng. Xem API bảng xếp hạng.
#Danh sách kiểm tra phát hành
- Chỉ kết nối HTTPS/WSS công khai của nền tảng và backend đáng tin cậy trong cấu hình build; không truy cập
/internal/*. - Kiểm tra cấu trúc đăng nhập mật khẩu, refresh, Ticket,
readyvà sự kiện. - Kiểm tra HTTP Bearer,
nbt.auth, gia hạn tại chỗ, hết hạn và URL đáng tin cậy của backend độc lập. - Chỉ giữ thông tin trong bộ nhớ, che log và telemetry.
- Refresh tuần tự và xử lý kết quả mạng không chắc chắn.
- Mỗi lần kết nối lại xin Ticket mới và cập nhật
leaseGeneration. comment,giftchống lặp bền vững bằngeventId; hiệu ứng lỗi không ACK.- Gom ACK 25ms / 100 sự kiện, một lô đang chờ trên mỗi kết nối.
- Phân biệt
debug_commandvới Productionevent. - Phân biệt ba mã có thể kết nối lại với mã kết thúc.
- Đăng xuất bằng DELETE và xóa toàn bộ thông tin trong bộ nhớ.