Build and deploy

Webhooks

Events IRT sends to your backend when rooms and players change, and how to verify them.

Set a project’s webhookUrl and IRT sends it an HTTP POST for each event in the project’s rooms, and when the project nears a spending cap. Set it in the dashboard, with irt webhooks set <url>, or with PATCH /projects/{p} and {"settings": {"webhookUrl": "https://..."}}. The URL must be https. A local or staging stack whose control server sets webhooks.allow_private_addresses also accepts http and private addresses, so a backend on the same machine can receive events.

Events

Type Sent when
room.created A room is created; data is {public, joinCode, retention}, where retention is the room’s own retention from POST /rooms, or null when it follows the project’s rooms.retention
room.active A room has started or woken and is running; data is {generation}
room.hibernated A room has hibernated; data is {generation}
room.restored A room woke on a build older than its room state, after a rollback, and started from its room state from before the migration; data is {generation, build, fromVersion, version}
room.deleted A room is deleted; data is {reason}: "api" when deleted through the API, the CLI or the dashboard, "expired" when its retention passed while hibernated, and "retention" when it emptied with retention "none"
player.joined A player’s connection joins a room; data is {player, connection}
player.left A player’s connection leaves a room; data is {player, connection, code}, with the close code
build.rolled_back The live build crash-looped and the build it replaced went live again; room is null and data is {build, to, reason, crashes}. See Automatic rollback
usage.alert The project passed 80% of a spending cap, at most once per cap per UTC day; room is null and data is {limit, used, max, day}, with limit maxRoomHoursPerDay or maxActiveRooms. See Stats and usage alerts
billing.low_balance Your account’s credit dropped below the low-balance threshold; sent to every project, once per drop, with room null and data {balance, threshold, graceEndsAt}. graceEndsAt is when projects stop waking rooms, if the balance is already at zero. See limits

The body is JSON:

{
  "type": "player.joined",
  "project": "my-game",
  "room": "forest-7",
  "at": "2026-11-04T10:00:00Z",
  "data": { "player": "u_42" }
}

Delivery

Each event is sent once, best effort. There are no retries, so a webhook that is down misses events. Treat webhooks as hints and read the HTTP API when you need the current truth, for example GET /projects/{p}/rooms/{id} for a room’s status.

Events normally arrive well under a second after they happen. A burst waits on your endpoint, which gets up to 32 deliveries at once, and events held while IRT’s services catch up after an outage arrive late, up to minutes. Order isn’t guaranteed. A room hibernates 30 seconds after its last player.left, so a read of the room on that event finds it running; a read that arrives late can wake it again.

Respond quickly with a 2xx; do slow work after responding.

Order

Events aren’t delivered in order. Different parts of IRT send them: the router sends room.created, room.deleted from the API and the player events, and the host running the room sends room.active, room.hibernated and room.restored. So a room created and started at once often sends room.active before room.created. Order a room’s events by at, and by generation where they have one.

generation grows by one at each attempt to start the room, including attempts that fail, so it can skip numbers. A room that failed to wake four times and then woke reports room.active with a generation four higher than its last.

Verifying a webhook

Every request has an IRT-Signature header:

IRT-Signature: t=1793786400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is the hex HMAC-SHA256 of t, a dot and the raw request body, keyed with the project’s webhook secret. To verify:

  1. Compute the HMAC over the raw body exactly as received, before parsing it.
  2. Compare it with v1 in constant time.
  3. Reject requests whose t is far from your clock, which stops a captured request being replayed later. Five minutes is a reasonable window.
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody: string, header: string, secret: string): boolean {
  const parts = new Map(header.split(",").map((kv) => kv.split("=", 2) as [string, string]));
  const t = parts.get("t");
  const v1 = parts.get("v1");
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

The webhook secret

Rotate the secret in the dashboard, with irt webhooks rotate-secret or with POST /projects/{p}/webhook-secret. The new secret is shown once and signs every event from then on, so update your backend straight away.

View this page as Markdown