> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.telekesher.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.telekesher.dev/_mcp/server.

# Webhook delivery

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](/webhooks/command-accepted-events).

| Category                       | Events                                                                                                                                                                                                                                                                                                                                           | Reference                                      |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| Call lifecycle                 | `call.created`, `command.call.dial.accepted`, `call.ringing_started`, `call.early_media_started`, `call.answered`, `call.ended`                                                                                                                                                                                                                  | [Call events](/webhooks/call-events)           |
| DTMF                           | `dtmf.received`, `digits.collected`                                                                                                                                                                                                                                                                                                              | [DTMF events](/webhooks/dtmf-events)           |
| Playback                       | `playback.started`, `playback.stopped`, `playback.failed`, `command.playback.pause.accepted`, `command.playback.resume.accepted`, `command.playback.seek.accepted`, `command.playback.restart.accepted`                                                                                                                                          | [Playback events](/webhooks/playback-events)   |
| Recording                      | `command.recording.start.accepted`, `recording.became_available`, `recording.ended`, `recording.failed`, `command.recording.mask.accepted`, `command.recording.unmask.accepted`, `recording.speech.started`, `recording.speech.ended`                                                                                                            | [Recording events](/webhooks/recording-events) |
| Rooms, members & room playback | `room.created`, `command.room.delete.accepted`, `room.deleted`, `room.member.*`, `room.playback.*`                                                                                                                                                                                                                                               | [Room events](/webhooks/rooms-events)          |
| WebSocket relay                | `websocket.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](/webhooks/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

| Header                | Value                              | Description                                                                                                                                                                                                                                                                                           |
| --------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`        | `application/json`                 | Always set. The request body is a JSON object.                                                                                                                                                                                                                                                        |
| `User-Agent`          | `Webhook-Delivery/1.0`             | Identifies 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-Signature` | `t={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](/webhooks/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](/webhooks/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.

| Parameter                        | Value             |
| -------------------------------- | ----------------- |
| Max delivery cycles per event    | `3`               |
| Max HTTP POST attempts per event | `5` (`2 + 2 + 1`) |
| Immediate retry backoff          | \~`500ms`         |
| Timeout per attempt              | `10 seconds`      |

### Which Responses Are Retried

| Response                                                                                  | Action                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `2xx`                                                                                     | Success. Event is acknowledged and removed from the queue.                                                                                                                                                                                                                                                                                                                                                                                            |
| `3xx`                                                                                     | **Redirects 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 Early`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/425) | **Active 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 `429`                                                                            | **Retryable 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 `4xx`                                                                               | **Permanent consumer failure.** The event is attempted once, placed directly in the dead-letter queue with `reason: consumer_rejected` and the numeric status, then acknowledged.                                                                                                                                                                                                                                                                     |
| `5xx`                                                                                     | **Retryable 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 error                                                      | **Retryable 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`](/webhooks/call-events#callanswered) before [`call.ended`](/webhooks/call-events#callended) 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`](/webhooks/rooms-events#roomcreated), [`command.room.delete.accepted`](/webhooks/rooms-events#commandroomdeleteaccepted), [`room.deleted`](/webhooks/rooms-events#roomdeleted)) and [room playback events](/webhooks/rooms-events#commandroomplaybackstartaccepted) are not associated with a `session_uuid` and have no ordering guarantee relative to session events. [`command.room.delete.accepted`](/webhooks/rooms-events#commandroomdeleteaccepted) is emitted immediately when Delete Room is accepted (`202`); [`room.deleted`](/webhooks/rooms-events#roomdeleted) follows later on confirmation, or with `reason:"timeout"` if confirmation never arrives.