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. data carries participantId (feed it to the approve / admission-poll endpoints) and externalUserId. Fired on first park only, like the in-call knock.
  • recording.completed — the recording file is confirmed in storage (unlike egress.ended, which only says the egress stopped). data.recordingId is what you pass to GET /recordings/{id}/download.
  • transcript.ready — an AI transcript reached COMPLETED. data carries transcriptId and recordingId.

By default you receive all; a tenant can subscribe to a subset (PATCH /tenant/webhookwebhookEvents). 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

HeaderMeaning
x-webhook-eventthe event name
x-webhook-idunique per event — dedupe on this
x-webhook-timestampthe signed timestamp (equals occurredAt)
x-webhook-versionpayload version (2)
x-webhook-signaturesha256=<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 is webhook.test. Works with vcp_test_ keys, and ignores your webhookEvents filter (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 original x-webhook-id, so a deduping consumer treats it idempotently.

On this page