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/bytes and 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.

ScopeWhat it reaches
rooms:readList rooms, read one room, list a room’s saves, read a save’s bytes
rooms:writeAll 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

QueryMeaning
active=trueOnly rooms awake in a live server
active=falseOnly 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, d30m, 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"
}
  • type is the room type parsed from the id (arena:7 is type arena; an id with no prefix is the default type). null if the id is not a room id.
  • class is the size class the current deployment declares for that type. Absent when the type declares no memoryMb, 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.
  • saves counts save generations across every deployment version.
  • stateBytes is 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.
  • deletesAt here 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"}'
retentionEffect
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
nullClears 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_FAILED naming 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.

RouteScopeWhat
GET /v1/projects/:id/rooms/:roomId/savesrooms:readSave generations, newest first
GET /v1/projects/:id/rooms/:roomId/saves/:saveId/bytesrooms:readOne generation’s raw bytes
POST /v1/projects/:id/rooms/:roomId/restorerooms:writeQueue 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

StatusCodeWhen
400E_BAD_REQUESTAn unknown query parameter, a filter that is not a duration, a PATCH field this route does not take
401E_UNAUTHENTICATEDNo key, a key that is not ours, or one that has been revoked. Unknown and revoked are worded identically on purpose
403E_SCOPEA valid key without the scope this route needs. The message names the scope
404E_NOT_FOUNDA project this key is not for, including one that does not exist
404E_ROOM_NOT_FOUNDA room with neither a row nor stored state
409E_ROOM_IN_USEAn awake room, deleted without ?force=true; or any room of a relay project with it
502E_EVICT_FAILEDWe 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.