Rooms API
Everything the dashboard shows you about a room, your own code can ask for. This is the API a nightly cleanup job, a support tool or your game’s own backend calls: list a project’s rooms, read one in full, keep one longer than its type says, and delete one, with or without disconnecting the players in it.
It is authenticated by a scoped API key you mint per project. A key is not a login. It reaches these routes and nothing else on the platform.
What is not here, and why
Room state is not readable or writable through this API. There is no JSON view of a room’s entities and there is no way to change one from outside.
That is a boundary rather than a missing feature. A room’s state is described by your schema, which changes when you deploy; a JSON projection of it would be a second, undeclared schema that every deploy could silently break, and a write path through it would be a second authority on state racing the room’s own tick. Rooms are the authority on their own state, and that is what makes the rest of the platform’s guarantees hold.
The two ways across the boundary, both of which already exist:
- To read state, fetch a save generation’s bytes with
GET /v1/projects/:id/rooms/:roomId/saves/:saveId/bytesand decode them with@irtio/server’s decode helper. Those are the exact bytes the room serialized, so they mean what your schema says they mean, in the version they were written under. - To change state, go through the room: connect a client with a server role, or call an RPC. Your room’s own code decides what is allowed, which is the point.
API keys
Mint one per job, with the narrowest scope that job needs.
irtio api-keys mint --scopes rooms:read --label "nightly cleanup" The key is printed once and never again: irtio stores only its SHA-256, so there is no command and no page that can show it to you a second time. If you lose it, revoke it and mint another.
A key looks like irk_ followed by 43 characters. The prefix is there so you can grep your logs
and your repository for one.
| Scope | What it reaches |
|---|---|
rooms:read | List rooms, read one room, list a room’s saves, read a save’s bytes |
rooms:write | All of the above, plus set retention, restore, and delete |
rooms:write implies rooms:read; you do not need both.
What a key cannot do
This is the list worth reading before you paste one into a CI variable. A key cannot:
- reach any project but the one it was minted for. Another project’s rooms answer
404, the same answer they give a stranger, so a key cannot even be used to find out that a project id exists; - mint, list or revoke keys, including itself. That is the property that bounds the damage of a leak to the scopes on the leaked key;
- deploy, roll back, read usage or billing, manage your fleet, touch your account, or reach the
CLI-token routes. Those all answer
401; - read or write player data, leaderboards or ratings.
A key never expires. Revoke it when the job that used it goes away:
irtio api-keys list
irtio api-keys revoke key_9f2c1a4b8d3e5f60 A revoked key’s row stays in the listing with the time it was revoked, because “what was this key
and who made it” outlives the key. lastUsed tells you whether a key you are about to revoke is
still in use.
Routes
Minting and managing keys needs your own login (a session or a CLI token), never a key.
# mint
curl -X POST https://irt.io/v1/projects/$PROJECT/keys
-H "authorization: Bearer $IRTIO_CLI_TOKEN"
-H 'content-type: application/json'
-d '{"scopes":["rooms:read"],"label":"nightly cleanup"}'
# list (no key material in the response, ever)
curl https://irt.io/v1/projects/$PROJECT/keys -H "authorization: Bearer $IRTIO_CLI_TOKEN"
# revoke
curl -X DELETE https://irt.io/v1/projects/$PROJECT/keys/$KEY_ID
-H "authorization: Bearer $IRTIO_CLI_TOKEN" Using a key
Send it as a bearer token:
curl https://irt.io/v1/projects/$PROJECT/rooms -H "authorization: Bearer $IRTIO_API_KEY" The irtio rooms commands also read IRT_API_KEY from the environment, so a cron job can use the
CLI without a stored login:
IRT_API_KEY=irk_… irtio rooms --project $PROJECT --idle --idle-longer-than 7d List rooms
GET /v1/projects/:id/rooms — scope rooms:read
| Query | Meaning |
|---|---|
active=true | Only rooms awake in a live server |
active=false | Only rooms that are not |
idle-longer-than=<duration> | Only rooms last reported longer ago than this |
A duration is one integer and one of m, h, d — 30m, 6h, 7d — from 1m to 3650d. No
spaces, no fractions, no compounds like 1h30m.
Both filters can be combined. A query parameter this route does not know is a 400 naming it,
rather than a filter that quietly did not apply, because the next line of a cleanup script usually
deletes what the filter returned.
curl "https://irt.io/v1/projects/$PROJECT/rooms?active=false&idle-longer-than=7d"
-H "authorization: Bearer $IRTIO_API_KEY" [
{
"project": "p_1a2b3c4d5e6f7080",
"roomId": "arena-7",
"status": "hibernated",
"shard": 0,
"lastSeen": "2026-08-30T11:02:41.000Z",
"clients": 0,
"maxClients": 8,
"backfill": false,
"retention": "7d",
"retentionOverride": null,
"deletesAt": "2026-09-06T11:02:41.000Z"
}
] retention is the window in force: the room’s own override when it has one, otherwise what its
type declares. null means the room’s state is kept forever, which is the default.
deletesAt is when the retention reaper will delete it, or null for never. On this route it is
approximate: it is measured from lastSeen, which is our clock and runs a few seconds behind the
storage timestamp the reaper actually uses. GET on one room gives the exact time. The difference
matters for a room whose server stopped reporting before it went to sleep; for everything else the
two agree to within a poll.
status is whatever the room’s server last reported. It is advisory: a room that has been asleep
for a long time may have had its row pruned and stop appearing in this listing entirely, while its
stored state is still there. Reading the room by id still works.
Read one room
GET /v1/projects/:id/rooms/:roomId — scope rooms:read
Everything on the list row, plus what only a per-room read can afford to work out. This route pays for one storage listing and answers the save count, the state size and the exact deadline from it.
{
"roomId": "arena-7",
"status": "hibernated",
"type": "arena",
"class": "medium",
"retention": "7d",
"retentionOverride": null,
"deletesAt": "2026-09-06T11:01:03.000Z",
"saves": 3,
"stateBytes": 41208,
"lastSeen": "2026-08-30T11:02:41.000Z"
} typeis the room type parsed from the id (arena:7is typearena; an id with no prefix is the default type).nullif the id is not a room id.classis the size class the current deployment declares for that type. Absent when the type declares nomemoryMb, which is a different fact from “small”: an undeclared room runs on the default heap and is billed at the Small rate, and this field answers what your project declared rather than what it is billed as.savescounts save generations across every deployment version.stateBytesis the size of the room’s stored state. Absent while the room is awake, because the stored copy is then whatever the last hibernation wrote and reporting it as the room’s current size would be a stale number presented as a fresh one. Read it after the room sleeps.deletesAthere is exact.
A room whose row has been pruned but whose state is still stored answers normally, with status: "hibernated". Only a room with neither a row nor a stored object is a 404.
Set a room’s retention
PATCH /v1/projects/:id/rooms/:roomId — scope rooms:write
The one thing you can change about a room from outside it is how long its state outlives its last activity.
curl -X PATCH https://irt.io/v1/projects/$PROJECT/rooms/arena-7
-H "authorization: Bearer $IRTIO_API_KEY"
-H 'content-type: application/json'
-d '{"retention":"30d"}' retention | Effect |
|---|---|
a duration (30m, 6h, 7d) | This room is kept that long past its last activity, whatever its type declares |
"forever" | This room is never deleted by retention |
null | Clears the override; back to what the type declares |
An override wins in both directions: 30d keeps a room its type would have deleted, and 1h disposes of one its type would have kept. The response is the same shape GET returns, so one
request reads back what it just wrote.
public is the other field this route takes: { "public": true } puts the room in the public
registry so a Join a game button can find it, and false takes it out again. It moves the same
row room.lobby.setPublic moves. A room whose game has already started answers 409 E_ROOM_STARTED, because a game in progress does not go back in front of strangers. See the lobby panel for the in-room side of the same flag.
Any other field is a 400 naming it. class is never a per-room property: a room’s server was
sized against its type’s declaration, so change the declaration and deploy. Room state is the
boundary above.
Delete a room
DELETE /v1/projects/:id/rooms/:roomId — scope rooms:write
Deletes the room’s stored state: its snapshot across every deployment version, its save generations and its alarm sidecar. Permanent. Player data and leaderboard scores are keyed to the player, not to the room, and are untouched.
curl -X DELETE https://irt.io/v1/projects/$PROJECT/rooms/arena-7
-H "authorization: Bearer $IRTIO_API_KEY" { "roomId": "arena-7", "objects": 5 } A room that is awake is refused with 409 E_ROOM_IN_USE, because deleting the state out from under
a live room would leave it serving something with no home to sleep back into.
?force=true
Adds the missing step rather than skipping the check. Before anything is deleted, we tell the
server holding the room to close every client in it with E_ROOM_DELETED (close code 4291) and drop the room. Only then does the
state go.
curl -X DELETE "https://irt.io/v1/projects/$PROJECT/rooms/arena-7?force=true"
-H "authorization: Bearer $IRTIO_API_KEY" Players in that room see a fatal error and their client stops rather than reconnecting, which is what keeps the room deleted.
Three things worth knowing:
- If we cannot reach the server holding the room, nothing is deleted. The answer is
502 E_EVICT_FAILEDnaming the server. “The room is still serving players and its state is gone” is the one outcome this whole exchange exists to prevent, so an uncertain eviction refuses the delete rather than proceeding. - A player who reconnects in the instant between the eviction and the delete gets a fresh room with the same id and no state. Nothing is corrupted: a fresh room has no stored state until it sleeps, so the delete that follows removes nothing of the new one’s.
- Relay projects are refused, because a relay room ends when its last client leaves and there is nothing to evict. Wait for it to empty.
Saves and restore
These are the existing week-12 routes; a key reaches them too.
| Route | Scope | What |
|---|---|---|
GET /v1/projects/:id/rooms/:roomId/saves | rooms:read | Save generations, newest first |
GET /v1/projects/:id/rooms/:roomId/saves/:saveId/bytes | rooms:read | One generation’s raw bytes |
POST /v1/projects/:id/rooms/:roomId/restore | rooms:write | Queue a restore from a generation |
A restore discards the room’s current state, and stops the project’s server if it is running, which briefly disrupts every other room in the project too. They come straight back from their own snapshots, unchanged. See saves.
Errors
| Status | Code | When |
|---|---|---|
| 400 | E_BAD_REQUEST | An unknown query parameter, a filter that is not a duration, a PATCH field this route does not take |
| 401 | E_UNAUTHENTICATED | No key, a key that is not ours, or one that has been revoked. Unknown and revoked are worded identically on purpose |
| 403 | E_SCOPE | A valid key without the scope this route needs. The message names the scope |
| 404 | E_NOT_FOUND | A project this key is not for, including one that does not exist |
| 404 | E_ROOM_NOT_FOUND | A room with neither a row nor stored state |
| 409 | E_ROOM_IN_USE | An awake room, deleted without ?force=true; or any room of a relay project with it |
| 502 | E_EVICT_FAILED | We could not reach the server holding the room to stop it. Nothing was deleted |
From the CLI
Everything above has a command:
irtio rooms --idle --idle-longer-than 7d
irtio rooms get arena-7
irtio rooms set arena-7 --retention 30d
irtio rooms set arena-7 --retention clear
irtio rooms delete arena-7 --force See the CLI reference.