ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Webhooks

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

MethodPathRequired roleDescription
GET/v1/developer/webhook-endpointsadmin+List endpoints
POST/v1/developer/webhook-endpointsadmin+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-secretadmin+Replace the signing secret
POST/v1/developer/webhook-endpoints/{endpoint_id}/testadmin+Send a signed test delivery
GET/v1/developer/webhook-deliveriesadmin+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"
}
FieldTypeDescription
idUUIDEndpoint id
labelstringDisplay name, 1–120 chars
urlstringHTTPS URL; http://localhost allowed for dev
eventsarray of stringSubscribed event types (see valid values). Empty array subscribes to all events except the explicit-only per-turn events (telephony.turn / web.turn)
statusstringactive, disabled (manually paused), or failing (auto-set when a delivery exhausts its 24 h retry schedule without a single 2xx)
agent_idinteger | nullAgent this endpoint is scoped to; null means organization-wide
agent_namestring | nullName of the scoped agent, or null for an organization-wide endpoint
secret_hintstringFirst 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_attimestamp | nullWhen ThunderPhone set the endpoint to failing. null while it is active or disabled
pause_last_errorstringThe last delivery error when the endpoint was paused, for example HTTP 503: Service Unavailable. Empty unless failing
pause_undelivered_countinteger | nullEvents not delivered since the endpoint's last successful delivery, counted when it was paused
created_at, updated_attimestamp

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.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

cURL
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
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
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

FieldTypeRequiredDescription
labelstringyes1–120 chars
urlstringyesHTTPS URL (http allowed only for localhost / 127.0.0.1)
eventsarraynoEmpty/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_idinteger | nullnoScope 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
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"]
  }'
FieldTypeDescription
labelstring
urlstring
eventsarray
statusstringactive or disabled. Set active to re-enable an endpoint the server marked failing
agent_idinteger | nullSet 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
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 '{}'
FieldTypeRequiredDescription
include_copiesbooleannoDefault 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-..."]
}
FieldTypeDescription
endpointobjectThe Endpoint object with its new secret_hint
secretstringThe new signing secret. Returned only in this response
rotated_endpoint_idsarray of UUIDEvery 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
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
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.