Open in
Webhook Endpoints
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, 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
{
"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). 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 |
Valid event types
events is validated against this exact set — values outside the list
return 400. See Events catalog for each type's
payload shape.
telephony.incoming,telephony.complete,telephony.tool,telephony.turnweb.incoming,web.complete,web.tool,web.turnsms.sent,sms.failedsms.received(reserved; not emitted yet)call.graded,call.data_extracted,call.transfer_completedcampaign.completedissue.reported,issue.escalatedtest-call.completedalert.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 viaPATCH. No requests are sent. We never change adisabledendpoint's status; flipping it back toactiveis 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,PATCHits status back toactive; deliveries whose retry schedule hasn't run out yet resume where they left off.
List endpoints
curl https://api.thunderphone.com/v1/developer/webhook-endpoints \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"Returns an array of Endpoint objects.
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
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"]
}'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"]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; 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 plus
an extra top-level secret field containing the raw signing key — a
48-character hex string:
{
"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"
}Update an endpoint
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"]
}'| 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.
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.
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 '{}'| 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:
{
"endpoint": { "id": "c4d5e6f7-...", "secret_hint": "7f3a…c21d", "...": "..." },
"secret": "7f3a90b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5c21d",
"rotated_endpoint_ids": ["c4d5e6f7-..."]
}| Field | Type | Description |
|---|---|---|
endpoint | object | The 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 |
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.
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-.../test \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"The endpoint receives an envelope like:
{
"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:
{
"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. 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.
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.
Omitting it preserves the generic webhook.test behavior.
Delete an endpoint
curl -X DELETE https://api.thunderphone.com/v1/developer/webhook-endpoints/c4d5e6f7-... \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"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.
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.