Skip to content

Operate

Webhooks: Room, Stream and Chat Events

Receive VoltKit RTC events on your server when rooms start and end, people join and leave, or streams go live, and verify each request's signature.

A webhook tells your backend what happened, when it happens. Without one you can only poll, which means learning about a join seconds late or calling the API constantly.

Use webhooks to record who watched and for how long, to drive "X is live" notifications, and to keep your own view of a room in step with reality.

Set one up

On the Webhooks tab of your project, enter an https URL and choose the events you want. You are shown a signing secret once, and your browser downloads it as a text file. You need it to verify every delivery: store it in your server's environment as VOLTKIT_RTC_WEBHOOK_SECRET, then delete the file. Saving the webhook again issues a new secret.

The endpoint must be reachable from the public internet. Private and local addresses are refused.

Events

EventFires whenNotable data
room.startedA room is createdhostIdentity
room.endedA room is closeddurationSeconds
participant.joinedSomeone joinsidentity, role
participant.leftSomeone disconnects, leaves or is removedidentity
role.changedA promotion or demotionrole, previousRole, by
track.publishedA publisher starts camera or microphonekind
track.unpublishedA track stops
stream.startedA broadcast stream goes on airstreamId
stream.endedA broadcast stream goes off airstreamId, durationSeconds
usage.limit_reachedThe project ran out of minutes or hit a usage limit while something was livereason, graceSeconds, endsAt
chat.messageA chat message is storedThe message

chat.message is off unless you tick it. A busy channel produces far more events than everything else combined.

What you receive

POST /hooks/voltkit HTTP/1.1
Content-Type: application/json
X-Stream-Event: participant.joined
X-Stream-Delivery: evt_9f2c41d0
X-Stream-Attempt: 1
X-Stream-Timestamp: 1787291928
X-Stream-Signature: t=1787291928,v1=9a3f...
{
  "id": "evt_9f2c41d0",
  "type": "participant.joined",
  "tenantId": "vk_4f2a9c0d1e3b5a67",
  "roomId": "show-42",
  "createdAt": 1787291928739,
  "data": { "identity": "user-99", "role": "viewer" }
}

tenantId is your App ID. For stream and chat events, roomId holds the stream id or the channel name.

Verify the signature

Always verify. Without it, anyone who finds your endpoint can post fake events to it.

The signature is an HMAC-SHA256 of "<timestamp>.<raw body>" using your signing secret. The timestamp is part of what is signed, so a captured request cannot be replayed later.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header ?? "");
  if (!match) return false;
  const [, timestamp, signature] = match;

  // Refuse anything old: this is what stops a replay.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)              // the RAW body, before JSON.parse
    .digest("hex");

  return expected.length === signature.length
    && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Delivery

  • Answer 2xx quickly, within 10 seconds, and do the work afterwards.
  • Failed deliveries are retried five more times, spaced 5 s, 30 s, 2 min, 10 min and 30 min apart.
  • A 4xx answer stops the retries, except 408 and 429. 410 Gone is treated as "stop sending".
  • Deliveries are at-least-once. The same event can arrive twice. Use id, which stays the same across retries, to ignore duplicates.
  • Order is not guaranteed. Use createdAt if order matters to you.

Test it

Press Send test event on the Webhooks tab. A signed webhook.test event is delivered to your endpoint, so you can confirm that it is reachable and that your signature check passes before any real traffic depends on it.

The same tab shows how many deliveries succeeded, how many were dropped after all retries, and the last error.

Last updated . Something unclear or wrong? Tell us.