Webhook delivery

Understand webhook security, retries, failures, and event ordering.
View as Markdown

The Gateway delivers webhook notifications to your server via HTTP POST. Verified domain facts describe observed state changes; command acknowledgements separately report commands accepted before their outcome is known.

Event Catalog

Webhook events are grouped by category. Each category has its own reference page documenting every event name, its payload fields, and when it fires:

All 20 command.*.accepted values use one shared CommandAcceptedEvent contract.

CategoryEventsReference
Call lifecyclecall.created, command.call.dial.accepted, call.ringing_started, call.early_media_started, call.answered, call.endedCall events
DTMFdtmf.received, digits.collectedDTMF events
Playbackplayback.started, playback.stopped, playback.failed, command.playback.pause.accepted, command.playback.resume.accepted, command.playback.seek.accepted, command.playback.restart.acceptedPlayback events
Recordingcommand.recording.start.accepted, recording.became_available, recording.ended, recording.failed, command.recording.mask.accepted, command.recording.unmask.accepted, recording.speech.started, recording.speech.endedRecording events
Rooms, members & room playbackroom.created, command.room.delete.accepted, room.deleted, room.member.*, room.playback.*Room events
WebSocket relaywebsocket.connected, websocket.disconnected, websocket.failed, websocket.message_received, websocket.audio_playback_completed, plus command acks (command.websocket.pause.accepted, command.websocket.resume.accepted, command.websocket.mute.accepted, command.websocket.unmute.accepted, command.websocket.flush.accepted)WebSocket events

Every event, regardless of category, carries a timestamp field (an RFC 3339 / ISO 8601 UTC string, e.g. 2026-06-28T14:30:00.000Z). It records the event time. Preserve same-session delivery order as described below. Delivery requests use W3C traceparent and tracestate headers for distributed tracing.

HTTP POST Delivery Format

Each webhook is delivered as an HTTP POST request with a JSON body. The request includes several headers for identification, deduplication, and security.

Request Headers

HeaderValueDescription
Content-Typeapplication/jsonAlways set. The request body is a JSON object.
User-AgentWebhook-Delivery/1.0Identifies the Gateway webhook delivery client.
X-Webhook-App-UUID{app_uuid}Application receiving the event. Require this header and compare it with your configured application UUID. Its exact value is covered by the signature.
X-Idempotency-Key{unique_key}Stable key for client-side deduplication across redeliveries of the same logical event. Treat it as opaque. Its exact value is covered by the signature; never trust or deduplicate on it until verification succeeds.
X-Webhook-Signaturet={unix},akid={kid},v1a={base64}Ed25519 signature header, always present. t is the Unix epoch seconds when the delivery was signed; akid identifies the cluster public key; v1a signs the exact bytes v1a\n{unix_seconds}\n{app_uuid}\n{idempotency_key}\n{raw_body}. See Webhook Signatures.

Signature Verification

Every delivery is signed with the cluster Ed25519 key so you can verify the request originated from the Gateway and was not tampered with. No per-app secret is needed — verification uses the cluster public key, fetchable from GET /.well-known/webhook-public-keys (unauthenticated). More than one key may be published during rotation (current + previous), so match on akid.

The X-Webhook-Signature header carries three required fields: t (Unix epoch seconds), akid (the cluster public-key id), and v1a (the standard-base64 Ed25519 signature). Require exactly one of each, reject duplicate known fields, and ignore unknown extension fields for forward compatibility. Require X-Webhook-App-UUID and X-Idempotency-Key, compare the application UUID with your configured value, and verify v1a over the exact LF-delimited bytes v1a\n<unix_seconds>\n<app_uuid>\n<idempotency_key>\n<raw_body>. Verify before parsing JSON, acting on the event, or consulting your deduplication store. Reject deliveries whose t is stale (for example, more than 5 minutes of skew) to limit replay.

Full field reference, replay protection, public-key fetching, key rotation, and ready-to-use Node.js / Python / Go verification code are in the dedicated Webhook Signatures guide.

Webhook Delivery & Retries

Delivery is at-least-once: the Gateway retries transient failures automatically, so the same logical event may reach your endpoint more than once. A retryable failure can result in up to 5 HTTP POST attempts across 3 delivery cycles:

  1. The first cycle starts as soon as the event is produced. After a retryable failure, the Gateway waits approximately 500ms and retries once within that cycle.
  2. If both HTTP attempts fail, an asynchronous retry worker starts the second cycle. A retryable failure on that cycle’s first attempt triggers one inline retry after approximately 500ms.
  3. The third and final cycle makes one HTTP attempt. If that attempt also fails with a retryable error, the event reaches retry exhaustion and is moved to the dead-letter queue.

Each individual attempt is allowed up to 10 seconds to complete before it is treated as a timeout.

ParameterValue
Max delivery cycles per event3
Max HTTP POST attempts per event5 (2 + 2 + 1)
Immediate retry backoff~500ms
Timeout per attempt10 seconds

Which Responses Are Retried

ResponseAction
2xxSuccess. Event is acknowledged and removed from the queue.
3xxRedirects are not followed. Configure the final HTTPS webhook endpoint directly. The Gateway never forwards the signed body or delivery headers to a redirect target. A redirect is a retryable redirect-policy failure under the normal delivery budget.
425 Too EarlyActive delivery lease. The Gateway treats every 425 as a deferral, stores the delay as a persistent not-before deadline, and leaves the event pending. A valid Retry-After supplies the bounded delay; a missing or invalid value defaults to 30 seconds. This does not consume a delivery cycle or HTTP-attempt budget and does not count as an endpoint failure. Use it when another handler still owns and renews the same delivery claim.
408 or 429Retryable response. It uses the same delivery budget as 5xx. A valid Retry-After can extend the inline retry delay, bounded to a maximum of 30 seconds.
Other 4xxPermanent consumer failure. The event is attempted once, placed directly in the dead-letter queue with reason: consumer_rejected and the numeric status, then acknowledged.
5xxRetryable failure. The Gateway retries inline when the current cycle has retry budget remaining. If no inline attempts remain, the asynchronous retry worker starts the next cycle.
Network / timeout / connection errorRetryable failure. DNS failures, refused connections, and timeouts are treated as transient and retried.

Build idempotent handlers. Because failed deliveries can cause an event to be re-sent, the same logical event may be delivered to your endpoint more than once. First verify the signature-covered raw body, application UUID, and idempotency key; only then consult or update your deduplication store. Process events for one session_uuid in the same-session delivery order guaranteed below; use timestamp as event-time metadata, not as a total-order key. Return 2xx after durably recording the event.

Undeliverable Events

Redirects are not followed: a redirect response is rejected without contacting its target and is treated as a retryable redirect-policy failure under the normal delivery budget. A final 5xx, 408, 429, or a connection, network, timeout, or redirect-policy failure can make up to 5 attempts across the same 3 cycles (2 + 2 + 1) because the first two cycles include one inline retry. A valid Retry-After on a 408, 429, or 5xx response can lengthen that inline delay, but never beyond 30 seconds. Every 425 instead records a persistent not-before deadline and defers without spending that delivery budget; its delay defaults to 30 seconds when Retry-After is missing or invalid. Other final 4xx responses are dead-lettered immediately with reason: consumer_rejected and the response status, without retry. Resolve the endpoint failure so subsequent events are accepted. When asking support to investigate a dead-lettered delivery, provide the event type, timestamp, X-Idempotency-Key, and any session_uuid or room_id in the verified event.

Per-Session Ordering Guarantees

Events for the same session_uuid are processed sequentially in the order they were produced. If an event has a retryable delivery failure, subsequent events for that session are held until the failed event succeeds or reaches retry exhaustion. A permanent consumer 4xx failure is dead-lettered immediately, and an exhausted retryable failure is moved to the dead-letter queue; in either case, delivery then continues with the next event. Among events your endpoint receives for one session, an earlier event is delivered before a later one — for example, call.answered before call.ended when both are delivered.

Events for different sessions may be delivered concurrently and can arrive interleaved or out of order relative to one another; ordering is guaranteed only within a single session_uuid (see above).

Room lifecycle events (room.created, command.room.delete.accepted, room.deleted) and room playback events are not associated with a session_uuid and have no ordering guarantee relative to session events. command.room.delete.accepted is emitted immediately when Delete Room is accepted (202); room.deleted follows later on confirmation, or with reason:"timeout" if confirmation never arrives.