Callback events
Game room events are delivered to your server as event callbacks: room created, match started, player got a seat, connected and disconnected, left, and room ended. Most games only need game_room.closed — it carries the settlement result and each player's online time, which you use to grant rewards, record match history and reconcile billing.
Add a callback URL on the console's "Callbacks" page and tick the game_room.* event types you need. Signature verification, retries and deduplication are the same as for other events, see callback security.

import { CallbackHandler, MemoryDedupStore, MemoryNonceStore } from '@deeprespond/im-server-sdk/callback';
const handler = new CallbackHandler({
secrets: { [process.env.IM_CALLBACK_ENDPOINT_ID!]: [process.env.IM_CALLBACK_SECRET!] },
nonceStore: new MemoryNonceStore(),
dedupStore: new MemoryDedupStore(), // 多台服务器时换成共享的存储
});
handler.on('game_room.closed', async (_event, data) => {
// data.result 是你结束房间时给的结算结果;房间超时、全员离开等自动结束时为 null
await saveMatch(data.room_id, data.close_reason, data.result);
for (const m of data.members) await addPlayMinutes(m.username, m.billable_minutes);
});For other languages see Go (EventHandlers.GameRoomClosed), Python (@handler.on_event("game_room.closed")) and Java (handler.on("game_room.closed", ...)); the full Node.js guide is in Node.js callbacks.
Events at a glance
| Event | When | Usage |
|---|---|---|
game_room.created | A room was created and assigned to a game node | As needed |
game_room.started | The match started; the game node began running frames | As needed |
game_room.member_joined | A player got a seat (the client called join) | As needed |
game_room.member_connected | A player connected to the game node for the first time | Frequent, subscribe if needed |
game_room.member_disconnected | A player dropped; they can come back within the reconnect window | Frequent, subscribe if needed |
game_room.member_left | A player left: voluntarily, timed out, removed, did not connect in time, or the room ended | As needed |
game_room.closed | The room ended, with the result and each player's online time | Recommended |
Common fields
Every event's data contains:
| Field | Description |
|---|---|
room_id | Room ID |
version | Room version, incremented on every status change. Events may arrive out of order or more than once; use it to tell newer from older |
ext | Extension fields given when the room was created, returned as-is |
origin | Who caused it: server your server, client a player, platform the platform, system the system (timeouts, nodes and so on) |
The outer id, type, occurred_at and other fields are described in event callbacks.
game_room.created
| Field | Description |
|---|---|
region | Region the room is in |
max_players, sync_mode, tick_rate | Settings at creation |
created_at | Creation time |
game_room.started
| Field | Description |
|---|---|
started_at | Start time |
player_count | Players seated when the match started |
game_room.member_joined
| Field | Description |
|---|---|
username, seat | Player and seat |
joined_at | When the seat was taken |
A player who does not connect to the game node within 60 seconds after getting a seat leaves with connect_timeout.
game_room.member_connected
| Field | Description |
|---|---|
username, seat | Player and seat |
transport | Connection type: udp or ws |
at | Connection time |
Sent only the first time this join connects; reconnecting after a drop does not send it again.
game_room.member_disconnected
| Field | Description |
|---|---|
username, seat | Player and seat |
reason | timeout no data for 5 seconds; network the connection was closed |
at | Disconnection time |
game_room.member_left
| Field | Description |
|---|---|
username, seat | Player and seat |
reason | leave left voluntarily; timeout dropped beyond the reconnect window; removed, kicked removed; connect_timeout did not connect in time after getting a seat; room_closed the room ended |
connected_ms | Total online time of this join (ms) |
left_at | Leave time |
game_room.closed
{
"room_id": "rank-8812",
"version": 6,
"origin": "server",
"ext": {"match_id": "m-8812"},
"close_reason": "closed_by_server",
"created_at": "2026-11-10T08:00:00.000Z",
"started_at": "2026-11-10T08:00:15.000Z",
"closed_at": "2026-11-10T08:31:02.000Z",
"duration_ms": 1847000,
"result": {"winner": "alice", "scores": {"alice": 12, "bob": 9}},
"members": [
{"username": "alice", "seat": 0, "connected_ms": 1845200, "billable_minutes": 31, "leave_reason": "room_closed"},
{"username": "bob", "seat": 1, "connected_ms": 1790100, "billable_minutes": 30, "leave_reason": "room_closed"}
]
}| Field | Description |
|---|---|
close_reason | Why the room ended, see room status |
created_at, started_at, closed_at | Times; started_at is null if the match never started |
duration_ms | Time from start to end (ms); 0 if the match never started |
result | The settlement result you gave when ending the room; null when it ended automatically |
members | One item per join (when a seat changed hands, each person has an item), ordered by seat and join order |
members[].connected_ms | Total online time of this join (ms) |
members[].billable_minutes | connected_ms rounded up to whole minutes, for your reconciliation and for timing players. Bills are summed per day and then rounded, so adding up matches may differ slightly, see billing |
members[].leave_reason | Why they left, same as reason in game_room.member_left |
How to use it:
- Find your match by
room_idor the match ID inext, then grant rewards and record history fromresult. The same event may be delivered again; deduplicate by eventid. - A
nullresultmeans you did not end the room (timeout, everyone left, node failure); treat it as a draw or void by your own rules. - On node failure (
node_lost) online time is counted up to the node's last report, at most 10 seconds less.
