---
title: "Webhook Endpoints"
description: "Manage multiple webhook URLs with per-endpoint secrets and event filters."
---

The endpoint-based webhook system lets you register **multiple**
destinations per organization, each with its own secret, its own
status, and its own subscription to a subset of event types. This is
the recommended model for all new integrations.

Compare to the [legacy single-URL webhook](/api-reference/organizations#legacy-single-url-webhook),
which is kept for backward compatibility but only supports one URL per
org.

## Endpoints

| Method | Path | Required role | Description |
|--------|------|---------------|-------------|
| `GET` | `/v1/developer/webhook-endpoints` | `admin+` | List endpoints |
| `POST` | `/v1/developer/webhook-endpoints` | `admin+` | Create an endpoint |
| `PATCH` | `/v1/developer/webhook-endpoints/{endpoint_id}` | `admin+` | Update label / URL / events / status |
| `DELETE` | `/v1/developer/webhook-endpoints/{endpoint_id}` | `admin+` | Delete an endpoint |
| `POST` | `/v1/developer/webhook-endpoints/{endpoint_id}/rotate-secret` | `admin+` | Replace the signing secret |
| `POST` | `/v1/developer/webhook-endpoints/{endpoint_id}/test` | `admin+` | Send a signed test delivery |
| `GET` | `/v1/developer/webhook-deliveries` | `admin+` | Inspect recent endpoint and legacy delivery results |

## Endpoint object

```json
{
  "id": "c4d5e6f7-...",
  "label": "Production — Call events",
  "url": "https://example.com/thunderphone/hook",
  "events": ["telephony.incoming", "telephony.complete"],
  "status": "active",
  "agent_id": 42,
  "agent_name": "Support Agent",
  "secret_hint": "a1b2…9f0e",
  "paused_at": null,
  "pause_last_error": "",
  "pause_undelivered_count": null,
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Endpoint id |
| `label` | string | Display name, 1–120 chars |
| `url` | string | HTTPS URL; `http://localhost` allowed for dev |
| `events` | array of string | Subscribed event types (see [valid values](#valid-event-types)). Empty array subscribes to all events except the explicit-only per-turn events (`telephony.turn` / `web.turn`) |
| `status` | string | `active`, `disabled` (manually paused), or `failing` (auto-set when a delivery exhausts its 24 h retry schedule without a single 2xx) |
| `agent_id` | integer \| null | Agent this endpoint is scoped to; `null` means organization-wide |
| `agent_name` | string \| null | Name of the scoped agent, or `null` for an organization-wide endpoint |
| `secret_hint` | string | First 4 and last 4 characters of the signing secret with an ellipsis (`a1b2…9f0e`) — enough to cross-reference the secret you saved locally without exposing the full value |
| `paused_at` | timestamp \| null | When ThunderPhone set the endpoint to `failing`. `null` while it is `active` or `disabled` |
| `pause_last_error` | string | The last delivery error when the endpoint was paused, for example `HTTP 503: Service Unavailable`. Empty unless `failing` |
| `pause_undelivered_count` | integer \| null | Events not delivered since the endpoint's last successful delivery, counted when it was paused |
| `created_at`, `updated_at` | timestamp | |

<Note>
  The endpoint's full `secret` is returned **once** on creation and
  never again. Store it securely — if you lose it,
  [rotate it](#rotate-the-signing-secret) to get a new one.
</Note>

### Valid event types

`events` is validated against this exact set — values outside the list
return `400`. See [Events catalog](/webhooks/events) for each type's
payload shape.

- `telephony.incoming`, `telephony.complete`, `telephony.tool`, `telephony.turn`
- `web.incoming`, `web.complete`, `web.tool`, `web.turn`
- `sms.sent`, `sms.failed`
- `sms.received` (reserved; not emitted yet)
- `call.graded`, `call.data_extracted`, `call.transfer_completed`
- `campaign.completed`
- `issue.reported`, `issue.escalated`
- `test-call.completed`
- `alert.triggered`

`issue.escalated` has no agent context and is delivered only to
organization-wide endpoints.

`voice.ready` and `voice.failed` cannot be selected explicitly. To receive
them, create an organization-wide endpoint with `events: []`. An empty event
list receives every supported event except `telephony.turn` and `web.turn`,
which must be selected explicitly.

### Endpoint statuses

- `active` — deliveries flow normally.
- `disabled` — manually paused via `PATCH`. No requests are sent. We
  never change a `disabled` endpoint's status; flipping it back to
  `active` is always your call.
- `failing` — set automatically when a delivery to the endpoint burns
  through its entire retry schedule (8 attempts over 24 hours) without
  ever getting a 2xx. A failing endpoint receives no further traffic.
  We email your organization's owners and admins when this happens, and
  the dashboard shows the paused endpoint with a Re-activate button.
  Once the endpoint is fixed, `PATCH` its status back to `active`;
  deliveries whose retry schedule hasn't run out yet resume where they
  left off.

---

## List endpoints

<CodeGroup>
```bash cURL
curl https://api.thunderphone.com/v1/developer/webhook-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

Returns an array of [Endpoint objects](#endpoint-object).
Pass `?agent_id=42` to return only endpoints scoped to that agent.

### Agent-scoped endpoints

Organization-wide endpoints receive every matching event. An endpoint with
`agent_id` receives only matching events for calls handled by that agent;
events without agent context, such as `alert.triggered`, never reach it. You
can also create and manage these endpoints from the agent builder's
**Webhooks** section.

---

## Create an endpoint

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label":  "Production — Call events",
    "url":    "https://example.com/thunderphone/hook",
    "events": ["telephony.incoming", "telephony.complete"]
  }'
```

```python Python
result = requests.post(
    "https://api.thunderphone.com/v1/developer/webhook-endpoints",
    headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
    json={
        "label":  "Production — Call events",
        "url":    "https://example.com/thunderphone/hook",
        "events": ["telephony.incoming", "telephony.complete"],
    },
).json()
secret = result["secret"]
endpoint_id = result["id"]
```
</CodeGroup>

### Request fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `label` | string | yes | 1–120 chars |
| `url` | string | yes | HTTPS URL (`http` allowed only for `localhost` / `127.0.0.1`) |
| `events` | array | no | Empty/omitted subscribes to all events except `telephony.turn` / `web.turn`, which require an explicit subscription. Must use the values listed in [Valid event types](#valid-event-types); duplicates are removed |
| `agent_id` | integer \| null | no | Scope delivery to an agent in this organization; omit or use `null` for an organization-wide endpoint |

Returns `201 Created` with the [Endpoint object](#endpoint-object) plus
an extra top-level `secret` field containing the raw signing key — a
48-character hex string:

```json
{
  "id": "c4d5e6f7-…",
  "label": "Production — Call events",
  "url": "https://example.com/thunderphone/hook",
  "events": ["telephony.incoming", "telephony.complete"],
  "status": "active",
  "secret_hint": "a1b2…9f0e",
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z",
  "secret": "a1b2c37e08d94f5b16a2c8d90e7f3a4b5c6d7e8f90a19f0e"
}
```

<Warning>
  `secret` is returned **only on creation**. Subsequent `GET` responses
  include only the `secret_hint`. Copy the full value to your secret
  manager before dismissing the response.
</Warning>

---

## Update an endpoint

<CodeGroup>
```bash cURL
curl -X PATCH https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-... \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label":  "Production — Call + Grade events",
    "events": ["telephony.incoming", "telephony.complete", "call.graded"]
  }'
```
</CodeGroup>

| Field | Type | Description |
|-------|------|-------------|
| `label` | string | |
| `url` | string | |
| `events` | array | |
| `status` | string | `active` or `disabled`. Set `active` to re-enable an endpoint the server marked `failing` |
| `agent_id` | integer \| null | Set an agent id to scope the endpoint, or `null` to make it organization-wide |

Returns `200 OK` with the updated [Endpoint object](#endpoint-object).

---

## Rotate the signing secret

Replace an endpoint's signing secret, for example when it was lost or
may have leaked. For an agent-scoped endpoint you can also use the
dashboard: open the agent's **Webhooks** section and choose
**Regenerate secret** on the endpoint. Organization-wide endpoints are
rotated through the API.

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-.../rotate-secret \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `include_copies` | boolean | no | Default `true`. Also gives the new secret to your organization's other endpoints that have the same URL and the same current secret, such as copies of this endpoint on other agents, so one receiver keeps verifying with one secret |

Returns `200 OK`:

```json
{
  "endpoint": { "id": "c4d5e6f7-...", "secret_hint": "7f3a…c21d", "...": "..." },
  "secret": "7f3a90b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5c21d",
  "rotated_endpoint_ids": ["c4d5e6f7-..."]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `endpoint` | object | The [Endpoint object](#endpoint-object) with its new `secret_hint` |
| `secret` | string | The new signing secret. Returned only in this response |
| `rotated_endpoint_ids` | array of UUID | Every endpoint that now uses the new secret |

<Warning>
  The old secret stops being used immediately: the next delivery,
  retries included, is signed with the new one. Until your receiver has
  the new secret, deliveries fail its signature check; if it answers
  those with a non-2xx status they are retried on the normal 24 h
  schedule, so update the receiver promptly. If your receiver rolls out
  in stages, have it accept both secrets during the rollout — the SDK
  verification helpers take a list of secrets (see
  [Verify webhook signatures](/guides/verify-webhook-signatures)).
</Warning>

---

## Send a test delivery

Send a synthetic `webhook.test` event to one endpoint using the normal
delivery pipeline, including canonical JSON serialization, both
signature headers, delivery recording, and retry bookkeeping.
The test targets the selected endpoint regardless of its `events` filter.

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-.../test \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

The endpoint receives an envelope like:

```json
{
  "data": {
    "message": "ThunderPhone webhook test",
    "sent_at": "2026-07-17T20:12:34.567890+00:00"
  },
  "event_id": "2ad6507c-7d19-4498-9b2d-7e8f944ab5a1",
  "type": "webhook.test"
}
```

The API returns `200 OK` after the first attempt, even if the destination
returns an error. Inspect `success`, `status`, `response_code`, and `error`
for the delivery outcome:

```json
{
  "success": true,
  "event_id": "2ad6507c-7d19-4498-9b2d-7e8f944ab5a1",
  "event_type": "webhook.test",
  "status": "delivered",
  "response_code": 204,
  "error": ""
}
```

`webhook.test` is synthetic and cannot be added to an endpoint's `events`
subscription. If the first attempt fails, ThunderPhone retries it once about
one minute later and then stops; the delivery then shows `status: "failed"`
under [Debug deliveries](#debug-deliveries). A test delivery never marks
the endpoint `failing`.

To configure a trigger against a real event shape, pass an optional
`event_type`. The delivery is still synthetic and contains `"sample": true`;
call-related samples use `call_id: 0` and `agent_id: 0`.

```bash
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-.../test \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_type":"call.graded"}'
```

`event_type` accepts any value from [Valid event types](#valid-event-types).
Omitting it preserves the generic `webhook.test` behavior.

---

## Delete an endpoint

<CodeGroup>
```bash cURL
curl -X DELETE https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-... \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

Returns `204 No Content`. Delivery to the URL stops immediately;
in-flight retries are abandoned.

---

## Debug deliveries

Before concluding that a webhook was not sent, inspect
[`GET /v1/developer/webhook-deliveries`](/api-reference/webhook-deliveries).
It shows recent attempts from both webhook systems, including the call id,
URL origin, HTTP status, attempt count, allowlisted failure category, and next
retry time. It never returns the event payload, transcript, stored error text,
response body, or URL path.

You can also see the same recent history in **Agents → select an agent → Webhooks →
Recent deliveries**. Rows show the endpoint label and the URL origin used by the latest
attempt. This is operational state rather than an immutable audit log: deleting an endpoint
also deletes its delivery rows.

For an n8n `404`, first confirm the workflow is active, accepts `POST`, and uses
the production webhook URL rather than the test URL. A `401` or `403` points to
authentication or signature validation; timeouts point to destination latency
or availability; TLS errors point to the certificate chain, hostname, or expiry.

---

## Related

<CardGroup cols={2}>
  <Card title="Events catalog" icon="list" href="/webhooks/events">
    The full list of `events` values you can subscribe to.
  </Card>
  <Card title="Webhooks overview" icon="bolt" href="/webhooks/overview">
    Signature verification and delivery semantics.
  </Card>
</CardGroup>
