Reference

HTTP API

The control server API at api.irt.io/v1 for projects, keys, secrets, builds, rooms, logs and usage.

https://api.irt.io/v1 takes and returns JSON. The dashboard, the CLI and your backend all use it.

Authentication

Credential Sent as Used by Acts for
Session cookie __Host-irt_session The dashboard at app.irt.io Your tenant
Personal token Authorization: Bearer irt_pat_... The CLI, after irt login Your tenant
API key Authorization: Bearer irt_key_... Backends and CI One project
curl https://api.irt.io/v1/projects/my-game/rooms \
  -H "Authorization: Bearer $IRT_API_KEY"

Keep API keys on your backend. Never ship one in a client.

Signing in

  • GET /auth/github?invite=... starts signing in with GitHub. The invite code is needed the first time only. The callback, /auth/github/callback, sets the session and returns to the dashboard.
  • POST /auth/logout ends the session.
  • GET /me returns the signed-in user, their tenant, its credit balance, and whether the user is an IRT admin.

Device flow

This is how irt login gets a personal token:

  1. POST /auth/device returns {deviceCode, userCode, verificationUrl}.
  2. The user opens https://app.irt.io/device, signs in and enters userCode.
  3. The CLI polls POST /auth/device/token with {deviceCode} until it returns {token}.

Errors

Errors have an HTTP status and a body of this shape:

{ "error": { "code": "not_found", "message": "No project named my-gmae." } }

code is a stable string to branch on, such as not_found, invalid, limit or unauthorized. message is for people and may change.

Projects

Method Path Body Does
GET /projects Lists your projects
POST /projects {slug, name} Creates a project
GET /projects/{p} Gets a project and its settings
PATCH /projects/{p} {name?, settings?}, with only the settings to change Updates a project
DELETE /projects/{p} Deletes a project

{p} is the project’s slug.

Project settings

Setting Default Allowed Meaning
players.guests true boolean Clients may join with no player token
players.createRooms true boolean Clients may create rooms and use join codes, and connecting to a room that doesn’t exist creates it
players.listPublic true boolean Clients may list active public rooms
players.quickJoin false boolean Clients may join any public room with space, or a new one
players.listPublicWithoutToken false boolean Anyone may list public rooms with no token, even with players.guests off
voice.enabled false boolean Connections get a voice token
voice.maxSpeakers 64 integer 1 to 64 A ceiling on each room’s own voice.maxSpeakers, applied from a room’s next wake; see Proximity voice
rooms.size "standard" "standard", "large", "extra-large" Room size for new rooms
rooms.retention "7d" "none", "<n>h", "<n>d" up to 365 days, "forever" How long a room is kept after it hibernates; "none" deletes it when it empties
rooms.buildUpdate "hibernate" "restart", "hibernate" Running rooms move to a new build at once, or on their next hibernate
rooms.rollbackOnCrashLoop true boolean When a newly live build crash-loops, make the build it replaced live again, once per build; see Builds
data none see Player data Player data keys clients may write, and the project’s collections and leaderboards; normally declared in irt.config.ts
limits.maxActiveRooms 100 integer Spending cap: rooms active at once; never from code
limits.maxRoomHoursPerDay 200 integer Spending cap: room-hours per day; never from code
limits.maxSleepingConnections 10000 integer, 0 to 1,000,000 Connections the project’s sleeping rooms hold in a region; never from code
webhookUrl none https URL Where webhook events go; never from code

Settings go under settings, in the same nested shape as the table, and null resets one to its default. Any other top-level field is a 400 invalid that says so:

curl -X PATCH $IRT_API_URL/projects/my-game \
  -H "Authorization: Bearer $IRT_API_KEY" -H "Content-Type: application/json" \
  -d '{"settings": {"webhookUrl": "https://example.com/irt", "players": {"guests": false}}}'

A setting the live build declares in its irt.config.ts can’t be changed here: a PATCH of it answers 409 naming that build.

A project’s client config, which the client SDK needs to connect, is its slug and its region’s router: {project, router: "wss://eu.irt.io"}.

Keys and secrets

Method Path Body Does
GET /projects/{p}/api-keys Lists API keys
POST /projects/{p}/api-keys {name} Creates an API key and returns it, once
DELETE /projects/{p}/api-keys/{id} Deletes an API key
GET /projects/{p}/signing-keys Lists signing keys: {signingKeys: [{id, createdAt}]}
POST /projects/{p}/signing-keys none Creates a signing key and returns {id, secret, createdAt} with 201; the secret is shown only this once
DELETE /projects/{p}/signing-keys/{id} Deletes a signing key; 204
POST /projects/{p}/webhook-secret Rotates the webhook secret and returns it, once
GET /projects/{p}/secrets Lists secret names
PUT /projects/{p}/secrets/{name} {value} Sets a secret
DELETE /projects/{p}/secrets/{name} Deletes a secret

Signing keys sign player tokens. Creating one takes no request body; a body sent anyway, such as {}, is ignored. A project has at most 5 signing keys, and creating a sixth answers 422 with the code limit. Signing keys are managed with a login or personal token, from the dashboard or irt signing-keys: an API key gets 403. Secrets reach room hosts as environment variables; their values are never returned. PATH, NODE_OPTIONS, HTTPS_PROXY, NODE_USE_ENV_PROXY and names starting with IRT_ are reserved, and setting one answers 400.

Builds

Method Path Body Does
POST /projects/{p}/builds the build tarball, application/gzip Uploads a build; returns it, with id, types and createdAt
GET /projects/{p}/builds Lists builds, newest first, with which is live
POST /projects/{p}/builds/{id}/live Makes a build live
POST /projects/{p}/restart {room?} Moves running rooms, or one, onto the live build now; returns {rooms}

Making a build live switches the site at once and moves rooms to it according to buildUpdate. The tarball’s layout is in Builds and deploys.

A build’s types are its room types by name, as its manifest declares them: each with schemaHash, and schema, stateVersion and entry for a standard type, or native, schema, stateVersion and tickRate for a native one, and its own size, maxPlayers and retention when it sets them. Making a build live returns it with schemaChecks, each type in both builds by name with its schema check, and schemaCheck, the default type’s or null. A refusal, 409 with code schema_change or type_removed, names the type in error.type.

Rooms

Method Path Body or query Returns
POST /projects/{p}/rooms {id?, type?, size?, retention?, public?, joinCode?, meta?, maxPlayers?} the room
GET /projects/{p}/rooms ?public&cursor active rooms
GET /projects/{p}/rooms ?status=all&cursor every room the project keeps, hibernated ones included
GET /projects/{p}/rooms/{id} the room, with its status
DELETE /projects/{p}/rooms/{id}
POST /projects/{p}/rooms/{id}/messages {message} the room’s reply
GET /projects/{p}/rooms/{id}/state ?meta=1 the room’s room.state; with meta=1, {state, tick, tickRate, savedAt}, where savedAt is when room state was last written (RFC 3339), for computing time-derived values; a room whose build uses an @irt/room older than ?meta=1 answers 422 room_error, so rebuild and deploy to use it
GET /projects/{p}/rooms/{id}/profile an active room’s tick profile over its last second, {window, profile}, per phase {count, total, p50, p95, max} in milliseconds; 409 room_not_active for a room that isn’t active, which isn’t woken for it; a build whose @irt/room predates it answers 422 room_error
POST /projects/{p}/rooms/{id}/kick {player} 204; closes the player’s connection with 4006
POST /projects/{p}/rooms/{id}/voice/mute {player, muted} 204; mutes or unmutes the player’s voice
POST /projects/{p}/rooms/{id}/voice/remove {player} 204; disconnects the player from the room’s voice

A room looks like this:

{
  "id": "forest-7",
  "status": "active",
  "build": "b_01J9Z3",
  "size": "standard",
  "public": true,
  "joinCode": "K7QX2M",
  "meta": { "mode": "coop" },
  "maxPlayers": 4,
  "players": 3,
  "createdAt": "2026-11-04T10:00:00Z",
  "stateBytes": 18342
}

status is active, waking or hibernated. stateBytes, only on GET /projects/{p}/rooms/{id}, is the size of the room’s room state as last saved to object storage, which trails a running room by up to 30 seconds; it’s null before the room’s first save. A room stops when its room state passes 32 MB. Leaving out id on create generates one. type is the room’s room type, one of the live build’s, else the type named default; a build without one answers 400 invalid naming its types. Without size, a room gets its type’s size, else the project’s rooms.size, and the same for maxPlayers and retention.

Creating a room doesn’t start it. It’s hibernated, with build: null, until its first connection or API call, which starts it on the live build like a wake: the client sees waking (the SDK’s onWaking and waking event), and the room runs onCreate, not onWake, since it has no room state yet. build is set from then on. A room created by connecting to an ID that doesn’t exist starts the same way. maxPlayers caps the room’s connections, from 1 to its size’s connection limit; connecting past it closes with 4003. It’s null when the room is capped only by its size. retention gives the room a retention of its own, with the same values as rooms.retention, which wins over the project’s: a game can keep its persistent rooms "forever" and let throwaway ones go after a day. Rooms clients create always follow the project’s.

A list is {rooms, cursor}; pass cursor back for the next page until it’s null. With ?status=all it lists every room the project keeps, sorted by ID, 500 a page: active rooms as above, and hibernated ones as {id, status: "hibernated", players: 0}. public can’t be combined with status=all. A room deleted through this API leaves the list at once. A room created moments ago by a client, or deleted by its retention, may be missing from it, or still in it, for a few seconds. Sending a message or reading state wakes a hibernated room, so the call may take as long as a wake, and the room then stays up for 30 seconds of room-hours before it hibernates again. There’s no read of a hibernated room’s stored state yet. To show a room’s data publicly without waking it on every view, keep a copy in a backend of your own: read the room while GET /projects/{p}/rooms/{id} says it’s active, and between reads compute what time changes from ?meta=1’s tick and savedAt. A site alone can’t do it, since it can’t hold an API key and /api on its domain is reserved. Don’t refresh on room.hibernated: that read wakes the room, which hibernates and sends room.hibernated again.

curl -X POST https://api.irt.io/v1/projects/my-game/rooms/forest-7/messages \
  -H "Authorization: Bearer $IRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": {"type": "grant", "player": "u_42", "item": "lantern"}}'

When the room can’t answer a message or a state read, the call answers 422 with code room_error, and message says why:

{ "error": { "code": "room_error", "message": "onRequest threw: unknown request" } }
  • onRequest threw: ... carries the message of the error your onRequest threw or rejected with. The room logs it at level error.
  • onRequest rejected: ... carries the message of a CallRejected your onRequest threw, for an expected refusal such as a move that breaks a rule. The room doesn’t log it.
  • onRequest may return a promise; the API waits for it.
  • unsupported call message means the room’s code has no onRequest.
  • Other messages say the room hibernated or stopped before it answered, or didn’t answer within 10 seconds. The call may or may not have run.

A room that can’t be woken answers 503 unavailable; when the cause is in the build, such as room code that throws on load or a room SDK the host doesn’t support, irt logs has a line saying why. A project over its spending limit or out of credit 429 limit, and a router that got no answer in time 504 timeout.

Kick and the voice calls answer 404 when the player isn’t connected, and the voice calls 409 with code voice_off when the region has no voice server. See Moderation.

Bans

Method Path Body Does
GET /projects/{p}/bans Lists the bans that haven’t expired
POST /projects/{p}/bans {player, reason?, expiresAt?} Bans a player, or replaces their ban; returns the ban
DELETE /projects/{p}/bans/{player} Unbans a player

A ban is {player, reason, expiresAt, createdAt}; expiresAt is null for a permanent ban, and must be in the future when given. A player ID is 1 to 134 bytes; URL-encode it in the path, so guest:ab12 is guest%3Aab12. A reason is at most 1000 bytes. A project can have 10,000 active bans; banning one more answers 422 limit. The router closes a banned player’s connections with 4013 within a few seconds and refuses new ones.

Data

Method Path Body Does
POST /projects/{p}/data one operation, at most 128 KB Runs a player data, collection or leaderboard operation as the server

The body is an operation, such as {"op": "player.get", "player": "ada", "key": "save"}; the operations are those of the room SDK, named player.get, player.set, player.increment, player.delete, player.list, entry.get, entry.put, entry.list, entry.delete, board.submit, board.top and board.around, with their arguments as fields and null meaning an optional one is left out. A backend may do every operation, and also player.purge, which deletes a player’s data and every entry they own, leaderboard scores included, and answers {keys, entries}, the numbers deleted: use it for a deletion request.

curl -X POST $IRT_API_URL/projects/my-game/data \
  -H "Authorization: Bearer $IRT_API_KEY" -H "Content-Type: application/json" \
  -d '{"op": "board.top", "collection": "highscores", "period": "week", "limit": 10}'

The answer is {"ok": true, "result": ...} or {"ok": false, "error": {"code": "E_...", "message"}}, with 200 either way, and 503 with code E_UNAVAILABLE when the data service can’t be reached within 5 seconds; a write that timed out may or may not have happened, so retry it with version. The codes are in Errors.

Clients reach the same operations, with a client’s rights, through the router: POST /v1/data/{p} on the region’s router with a player or guest token as Authorization: Bearer, and POST /v1/guest with {project} for a guest token, {token, player}, at most 10 a minute per address. The client SDK’s dataClient makes these calls.

Logs and usage

  • GET /projects/{p}/logs?since=...&follow=1 returns room host output. With follow, it’s a stream of server-sent events.
  • GET /projects/{p}/usage?from=...&to=... returns usage by day: room-hours by room size, traffic, voice-minutes and cost.
  • GET /projects/{p}/stats?from=...&to=... returns each build’s peak players and rooms, reconnects, and the errors and tick cost its rooms report, over a range of at most 14 days. GET /projects/{p}/builds/{id}/stats returns one build’s per minute, over at most 24 hours. See Stats and usage alerts.
  • GET /projects/{p}/alerts returns today’s usage alerts.

View this page as Markdown