> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive meeting session events on your own endpoint and verify their signatures.

Instead of polling, point the bot at your own endpoint. Set `webhook_url` when creating the session and Telnyx pushes session events to it. The destination must be a public HTTPS URL without embedded credentials and must not resolve to loopback, private, link-local, or otherwise reserved addresses; redirects are not followed. Each session returns a `webhook_secret` exactly once at creation -- store it; it is not retrievable again.

## Envelope and Events

Every delivery is an HTTP `POST` with `Content-Type: application/json`, no authorization header, and one envelope shape -- deduplicate on `id`:

```json theme={null}
{
  "id": "whdel_9f2c...",
  "event": "session.status_changed",
  "version": "1",
  "occurred_at": "2026-06-16T09:00:05Z",
  "data": {}
}
```

| Event                    | `data` fields                                                              |
| ------------------------ | -------------------------------------------------------------------------- |
| `session.status_changed` | `session_id`, `status`, `status_detail`, `recording`                       |
| `transcript.completed`   | `session_id`, `segment_count`, `last_seq`, `ended_at`                      |
| `recording.available`    | `session_id`, `recording_types` (never URLs)                               |
| `artifact.completed`     | `session_id`, `artifact_id`, `type`, `content: {text}`, `model_provenance` |
| `artifact.failed`        | `session_id`, `artifact_id`, `type`                                        |

Each event's full payload schema lives in the **Webhooks** group of the API Reference. Individual `transcript.segment` messages are never delivered over webhooks. Read segments via the [transcript endpoint](/docs/meeting/live-transcript) or the stream.

## Verify Signatures

Each delivery is signed with an `X-Meeting-Bot-Signature` header:

```
X-Meeting-Bot-Signature: t=1784297100,v1=5e8f...a3c1
```

Compute HMAC-SHA256 over the timestamp's ASCII bytes, a literal `.` byte, and the exact raw request-body bytes, then hex-encode the result and compare it against `v1`. Do not parse and reserialize the JSON before verifying.

```python theme={null}
import hashlib
import hmac

header = "t=1784297100,v1=5e8f...a3c1"
raw_body = request.body  # exact bytes as received

t = header.split(",")[0].split("=")[1]
v1 = header.split(",")[1].split("=")[1]

expected = hmac.new(
    webhook_secret.encode(),
    t.encode() + b"." + raw_body,
    hashlib.sha256,
).hexdigest()

verified = hmac.compare_digest(v1, expected)
```

Concatenate the timestamp and body as bytes. Formatting `raw_body` into a string (for example with an f-string) produces `b'...'` in the signed payload and the comparison always fails.

Use a constant-time comparison and reject deliveries whose timestamp is outside your accepted clock skew window.

## Delivery and Retry

* Up to 5 delivery attempts. Non-2xx responses, redirects, network errors, and timeouts all consume an attempt.
* Retries back off: roughly a minute after the first failed attempt, doubling each retry (about `60s * 2^n`).
* Each attempt has a 10-second timeout, so respond with a 2xx promptly.
* Events are delivered with best effort and duplicates can occur -- including when your endpoint processed an event but its response was lost. Deduplicate on the event `id` and fall back to REST polling when guaranteed receipt matters.

## Related

* [Session Events](/docs/meeting/events) -- the full event history, and what to read when a delivery is missed
* [Join a Meeting](/docs/meeting/join-meeting) -- set `webhook_url` when creating a session
* [Collect Results](/docs/meeting/collect-results) -- read the finalized transcript and artifacts
