Webhooks
Receive signed events and verify every delivery.
The platform POSTs events to your webhookUrl as JSON. Verify every delivery.
Events
Relayed from the media layer: room.started, room.finished,
participant.joined, participant.left, egress.started, egress.ended,
ingress.started, ingress.ended.
Platform-originated:
participant.waiting— someone landed in a waiting room.datacarriesparticipantId(feed it to the approve / admission-poll endpoints) andexternalUserId. Fired on first park only, like the in-call knock.recording.completed— the recording file is confirmed in storage (unlikeegress.ended, which only says the egress stopped).data.recordingIdis what you pass toGET /recordings/{id}/download.transcript.ready— an AI transcript reached COMPLETED.datacarriestranscriptIdandrecordingId.
By default you receive all; a tenant can subscribe to a subset
(PATCH /tenant/webhook → webhookEvents). Media-layer track-level events
are intentionally not forwarded. A webhook.test event type exists only for
the self-test endpoint below.
Envelope
{
"version": 2,
"id": "evt_…",
"event": "participant.joined",
"occurredAt": "2026-08-12T10:00:00.000Z",
"data": { "…": "…" }
}Headers
| Header | Meaning |
|---|---|
x-webhook-event | the event name |
x-webhook-id | unique per event — dedupe on this |
x-webhook-timestamp | the signed timestamp (equals occurredAt) |
x-webhook-version | payload version (2) |
x-webhook-signature | sha256=<hex>, or several comma-separated during a secret rotation |
Verify the signature
The HMAC-SHA256 input is `${timestamp}.${rawBody}` — the timestamp is
authenticated, which lets you reject replays. Accept the delivery if any
signature in the comma-separated header matches. Then check the timestamp is
fresh (reject if older than your tolerance, e.g. 15 minutes) and dedupe on
x-webhook-id.
const sig = 'sha256=' + hmacSha256(secret, `${timestamp}.${rawBody}`);
const ok = header.split(',').some((c) => timingSafeEqual(c.trim(), sig));Delivery & retries
Up to 6 attempts with exponential backoff (base 5s), 10s per-attempt
timeout. Respond 2xx to acknowledge. Retries reuse the same x-webhook-id,
so your dedupe makes them safe.
Debugging tooling
You never need to ask what was sent — with your tenant key:
POST /v1/webhooks/test(tenant.manage) — sends a synthetic signed event through the real queue/signer/retry pipeline. Pass{ "event": "recording.completed" }to exercise your routing for a specific type; default iswebhook.test. Works withvcp_test_keys, and ignores yourwebhookEventsfilter (you are testing the pipe, not the subscription).GET /v1/webhooks/deliveries(tenant.read) — paginated log of every delivery attempt: exact envelope, your response code/body, retry state. Filters:?event=,?status=delivered|failed|retrying.POST /v1/webhooks/deliveries/{id}/replay(tenant.manage) — re-delivers with the originalx-webhook-id, so a deduping consumer treats it idempotently.