Server API
Your server manages game rooms with an App Token: list regions, create rooms, issue join tickets, start, end, remove players, and query rooms and members. Joining a room and connecting to the game node are done by the client SDK and are not covered here. The paths below omit the app prefix; the full URL is $IM_API/$ORG/$APP followed by the path. Authentication is described in authentication and App Token.
The three most common steps — create a room, issue tickets, end it:
curl -X PUT "$IM_API/$ORG/$APP/game/rooms/rank-8812" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d '{"max_players":4,"start_mode":"all_ready","ext":{"match_id":"m-8812"}}'
curl -X POST "$IM_API/$ORG/$APP/game/rooms/rank-8812/tickets" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d '{"users":[{"username":"alice"},{"username":"bob"},{"username":"carol"},{"username":"dave"}]}'
curl -X POST "$IM_API/$ORG/$APP/game/rooms/rank-8812/close" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d '{"result":{"winner":"alice"}}'API at a glance
| To do this | Method and path | Retry |
|---|---|---|
| List available regions | GET /game/regions | Yes |
| Create a room | PUT /game/rooms/{room_id} | Yes, the same parameters return the same room |
| Issue join tickets | POST /game/rooms/{room_id}/tickets | Not retried automatically; after a timeout simply issue again |
| Start | POST /game/rooms/{room_id}/start | Yes |
| End a room | POST /game/rooms/{room_id}/close | Yes, only the first call takes effect |
| Remove a player | POST /game/rooms/{room_id}/members/{username}/remove | Yes, with the same operation_id |
| Get a room | GET /game/rooms/{room_id} | Yes |
| List rooms | GET /game/rooms | Yes |
| List members | GET /game/rooms/{room_id}/members | Yes |
Server SDKs
All four server SDKs have a game module whose methods map one-to-one to the API. They exchange App Tokens automatically and retry requests that are safe to retry:
| API | Go | Node.js | Python | Java |
|---|---|---|---|---|
| List regions | client.Game.ListRegions | client.game.listRegions | client.game.list_regions | im.game().listRegions |
| Create a room | Game.CreateRoom | game.createRoom | game.create_room | game().createRoom |
| Issue tickets | Game.IssueTickets | game.issueTickets | game.issue_tickets | game().issueTickets |
| Start | Game.StartRoom | game.startRoom | game.start_room | game().startRoom |
| End a room | Game.CloseRoom | game.closeRoom | game.close_room | game().closeRoom |
| Remove a player | Game.RemoveMember | game.removeMember | game.remove_member | game().removeMember |
| Get a room | Game.GetRoom | game.getRoom | game.get_room | game().getRoom |
| List rooms | Game.ListRooms, AllRooms | game.listRooms, iterRooms | game.list_rooms, iter_rooms | game().listRooms |
| List members | Game.ListMembers | game.listMembers | game.list_members | game().listMembers |
A Node.js example — create a room and issue tickets after a match is made, and settle when it ends:
import { DRClient, isDRError } from '@deeprespond/im-server-sdk';
const im = new DRClient({ baseUrl: process.env.IM_API!, orgName: 'demo-org', appName: 'demo', clientId: process.env.IM_CLIENT_ID!, clientSecret: process.env.IM_CLIENT_SECRET! });
export async function startMatch(matchId: string, players: string[]) {
try {
// 房间 ID 用匹配 ID:超时重试也只会有一个房间
await im.game.createRoom(matchId, { max_players: players.length, start_mode: 'all_ready', ext: { match_id: matchId } });
} catch (err) {
if (isDRError(err) && err.reason === 'game_capacity_unavailable') throw new Error('游戏服务繁忙,请稍后重试');
throw err;
}
const tickets = await im.game.issueTickets(matchId, { users: players.map((username, seat) => ({ username, seat })) });
return Object.fromEntries(tickets.map((t) => [t.username, t.ticket])); // 分别交给每个玩家
}
export async function finishMatch(matchId: string, winner: string) {
const { accepted } = await im.game.closeRoom(matchId, { reason: '对局结束', result: { winner } });
return accepted; // false 表示房间之前已经在结束
}Full examples, method signatures and error handling per language: Go, Node.js, Python, Java; installing and initializing is covered in server SDKs. The sections below describe each API's request and response.
List regions
GET /game/regions
{"items": [{"region": "cn-east", "name": "华东", "default": true, "available": true}]}| Field | Description |
|---|---|
region | Region ID, used when creating rooms |
default | Whether this is the app's default region, used when a room is created without a region |
available | Whether game nodes can currently take rooms in this region. When false, creating a room there fails |
Create a room
PUT /game/rooms/{room_id}. room_id is 1–64 letters, digits, _ or -, case-sensitive. Every field of the request body is optional (send {} when omitting all):
| Field | Default | Description |
|---|---|---|
name | empty | Room name, up to 128 characters, shown only in the console |
region | the app's default region | Region ID. Use either this or regions |
regions | — | 1–3 candidate regions tried in order; the first one with an available node wins. Useful for ordering by where the players are |
max_players | 10 | Number of seats, 1–10 |
sync_mode | frame | frame for frame sync; relay only relays custom messages, see sync mode |
tick_rate | 30 | Frame rate: 10, 15, 20, 30, 60. Ignored for relay. Higher rates mean lower latency and more traffic |
start_mode | auto | auto starts when the first player connects; all_ready starts when the connected players are all ready; manual starts when you call start |
access_mode | ticket | ticket requires a ticket you issue; open lets any signed-in user of the app join without a ticket |
max_duration_seconds | 3600 | Maximum duration, at least 60 and at most the platform limit (shown on the console's "Game rooms" page). The room ends automatically when it is reached (expired) |
reconnect_window_seconds | 60 | How long a seat is kept after a disconnect, 0–300; 0 means a disconnect is a leave |
snapshot | off | {"enabled": true, "interval_seconds": 30}: state snapshots for frame-sync rooms, used to catch up when a player was away too long and too many frames are missing. Interval 10–120 seconds. Not allowed for relay |
ext | {} | Your extension fields (a JSON object), up to 2KB, returned as-is in callbacks. Good for a match ID or mode |
Response: 201 when created, 200 when a room with the same parameters already exists (a repeated request); both return {"room": Room}. Room fields are in room.
Room ID rules: creating the same ID with the same parameters again returns the same room; different parameters, or an ID whose room has already ended, return 409 conflict (room_id_used). So when you use the match ID as the room ID, your matchmaking service can retry safely.
Issue join tickets
POST /game/rooms/{room_id}/tickets
{"users": [{"username": "alice", "seat": 0}, {"username": "bob"}], "ttl_seconds": 120}| Field | Description |
|---|---|
users | 1–10 users. seat is optional and pins a seat (0 to max_players-1); without it the player gets the lowest free seat when joining |
ttl_seconds | Validity, 30–600 seconds, default 120 |
Response 201:
{"tickets": [
{"username": "alice", "seat": 0, "ticket": "gtk_9f3KqZ1mXb2…", "expires_at": "2026-11-10T08:02:00.000Z"},
{"username": "bob", "seat": null, "ticket": "gtk_Hq7Lm0Pz4c…", "expires_at": "2026-11-10T08:02:00.000Z"}
]}- The raw
ticketis returned only here, once. Hand it to the right player through your business API and do not write it to logs. - A ticket can be used once and is valid only for this user and this room.
- Issuing again for the same user revokes their unused ticket. So after a timeout just issue again; the server SDKs do not retry this API automatically.
openrooms need no tickets; issuing returns400.
Start
POST /game/rooms/{room_id}/start, no request body.
Only for rooms whose start_mode is manual. The room becomes playing once the game node starts running frames, so room.status may still be open in the response; a room already in a match is returned as-is.
End a room
POST /game/rooms/{room_id}/close
{"reason": "对局结束", "result": {"winner": "alice", "scores": {"alice": 12, "bob": 9}}}| Field | Description |
|---|---|
reason | Optional, up to 200 characters, written only to the operation log; players do not see it |
result | Optional, your settlement result (a JSON object), up to 4KB. Sent as-is to every player in the room (the result of onClose on the client) and included in the game_room.closed callback |
Response: 202 means this call started ending the room, which becomes closing; once the game node finishes it becomes closed and game_room.closed is sent. 200 means the room was already ending or ended; the current room is returned and the first reason and result are kept.
Without calling this API, rooms still end automatically on timeout or when everyone has left, see room status.
Remove a player
POST /game/rooms/{room_id}/members/{username}/remove
{"operation_id": "kick-8812-bob-1", "reason": "作弊"}| Field | Description |
|---|---|
operation_id | Idempotency key, 1–64 letters, digits or _ . : -. Repeating a request with the same operation_id returns the first result. The server SDKs generate one when omitted and keep it across retries |
reason | Optional, up to 200 characters, written to the operation log |
Response: 202 {"operation_id": "…", "member": Member} means the player was removed; 200 {"operation_id": "…", "member": null} means the user is not in the room.
The removed player is disconnected immediately; reason in the client's onClose is kicked and the SDK does not reconnect. Their unused tickets are revoked as well. To let them back, issue a new ticket.
Get a room
GET /game/rooms/{room_id} returns {"room": Room}.
List rooms
GET /game/rooms?status=open,playing&search=rank-&limit=20&cursor=…
| Parameter | Description |
|---|---|
status | open, playing, closing, closed, comma-separated; all when omitted |
search | Room ID prefix |
limit, cursor | Paging; limit 1–100, default 20; for the next page pass the returned next_cursor, null means no more |
Newest first by creation time; returns {"items": [Room], "next_cursor": "…"}.
List members
GET /game/rooms/{room_id}/members returns {"items": [Member]}. There is one item per join: a seat taken over by someone else, or the same person leaving and joining again, produces several items, in join order.
Room
| Field | Description |
|---|---|
room_id, name | Room ID and name |
status | open, playing, closing, closed |
region, region_name | Region the room is in |
max_players, player_count | Number of seats and players currently seated |
sync_mode, tick_rate, start_mode, access_mode | Settings at creation; tick_rate is 0 for relay rooms |
max_duration_seconds, reconnect_window_seconds, snapshot | Settings at creation |
ext | Extension fields given at creation |
version | Room version, incremented on every status change; use it to tell newer callbacks from older ones |
created_at, started_at, closed_at | Times (ISO 8601, UTC); null when not started or not ended |
close_reason | Why the room ended, see room status; an empty string while not ended |
result | Settlement result given when ending; null when none |
Member
| Field | Description |
|---|---|
seat, username | Seat and username |
status | joined has a seat but has not connected; connected online; disconnected dropped (within the reconnect window); left has left |
transport | Connection type: udp or ws; an empty string if never connected |
rtt_ms | Median round-trip time (ms), only while connected |
connected_ms | Total online time (ms), the basis for billing |
joined_at, left_at | Join and leave times |
leave_reason | leave left voluntarily; timeout dropped beyond the reconnect window; removed, kicked removed; connect_timeout did not connect within 60 seconds after getting a seat; room_closed the room ended |
Errors
The error format is described in error codes. The specific reason for game rooms is in details.reason:
| HTTP | code | details.reason | Cause and what to do |
|---|---|---|---|
| 403 | permission_denied | game_disabled | The platform has not opened game rooms, or disabled them for your app. Contact us |
| 403 | permission_denied | plan_feature_disabled | Your plan does not include game rooms. Upgrade the plan |
| 400 | invalid_argument | region_unavailable | The region does not exist or is disabled. Use a region returned by list regions |
| 400 | invalid_argument | — | Invalid parameters; message names the field. When issuing tickets: a user does not exist or is disabled (details.usernames), or the same seat was pinned twice in one batch (details.seat) |
| 404 | not_found | — | The room does not exist |
| 409 | conflict | room_id_used | The room ID is used by a room with different parameters, or that room has ended. Use another ID |
| 409 | conflict | room_not_open | The room is ending or has ended; tickets can no longer be issued and it cannot be started |
| 409 | conflict | operation_id_used | The operation_id was used for another removal. Use a different one |
| 409 | limit_exceeded | open_rooms | The number of concurrent rooms reached the limit. End rooms you no longer need, or contact us to raise the limit |
| 503 | unavailable | game_capacity_unavailable | The region has no available game node right now. Retry later, or give candidate regions with regions |
| 503 | unavailable | game_node_unavailable | The game node hosting the room is temporarily unavailable |
