Webhooks
This reference page specifies how webhook deliveries work: the event format, delivery guarantees, headers, signatures and retries. For step-by-step instructions, see Receiving webhooks, Verifying webhook signatures and Retry and reliability.
Endpoints
A webhook endpoint is an HTTPS URL on your server, registered with POST/webhook-endpoints (scope webhooks:write) or on the console's API & Developers page, with the event types it should receive.
- An endpoint belongs to one mode: one registered with a test key receives only test-mode events (
"livemode": false), one registered with a live key only live events. Register separate endpoints for each. - The URL must be public HTTPS on port 443, without credentials. It is checked when you register it and again on every delivery; a URL that is not allowed answers
webhook_url_not_allowed. - The signing secret (
whsec_...) is returned once, in the first response when you create the endpoint or rotate its secret. Store it like an API key. A replay of either request with the sameIdempotency-Keyreturns the endpoint withsecret: null: the secret is never stored in readable form. If you lose it, rotate the secret. - Rotating the secret (POST
/webhook-endpoints/{webhook_endpoint_id}/rotate-secret) always issues a new current secret, returned once. At most two secrets ever sign a delivery: the current one and, while a grace period runs, one previous one.grace_period_hours: 0is always allowed, also during a running grace period, and retires every previous secret at once: only the new secret signs from the next delivery. Use it when a secret may have leaked.- A grace period (
grace_period_hoursabove 0) with no grace period running keeps the old secret signing until the grace period ends. - A grace period while another one is running keeps the oldest still-valid secret, until its original end (not extended), and discards the secret issued in between. So if you lost a rotation's response, rotate again: receivers still on the oldest secret keep verifying meanwhile.
- An endpoint is
enabledordisabled; disabled endpoints receive nothing. - Webhook delivery is in private preview and is switched on for the platform as a whole, separately from API access.
capabilities.webhooksin GET/workspacesays whether it is on. While it is off, endpoints cannot be created, test events and retries are refused withwebhooks_not_enabled, and no events are sent.
Events
Every delivery is a POST with one event as its JSON body:
JSON
(The call object is shortened here; events carry the full object.)
| Field | Meaning |
|---|---|
id | Event id (evt_...). The same event can be delivered more than once: use it to discard duplicates. |
type | The event type. |
created_at | When the event happened. |
livemode | The event's mode. |
api_version | The payload format version, today 2026-10-21. |
data.object | A snapshot of the object (a call or a campaign) when the event happened, in the same shape as the API returns it. |
Event types in v1:
| Type | Sent when |
|---|---|
call.ended | A call ended, whatever the outcome. Outcome and duration are known but can still change until call.settled; artifacts and the charge may still be pending. |
call.settled | Everything about a call is final, including transcript, recording, analysis and the charge. |
campaign.started | A campaign started a run and began dialling. |
campaign.paused | A campaign was paused, by a request or automatically (budget, balance, telephony). |
campaign.resumed | A paused campaign resumed its run. |
campaign.stopped | A campaign was stopped. |
campaign.completed | A campaign run finished dialling every recipient. |
campaign.failed | A campaign run ended in failure before finishing. |
webhook_endpoint.test | A test event you sent to an endpoint (see below). |
Send a webhook_endpoint.test event to an endpoint with POST/webhook-endpoints/{webhook_endpoint_id}/test. The full payload of each type is on the Event types page. New event types can be added at any time; ignore types you do not know and still answer 2xx.
Delivery
- At least once. The same event can arrive more than once, for example when your answer was lost. Discard duplicates by event
id(also in theRelationsAI-Event-Idheader). - Unordered. Events about different objects, and sometimes about the same object, can arrive in any order. Decide what is newer from the object itself (
status,settled, timestamps), not from arrival order. If in doubt, read the object from the API. - HTTPS only, and redirects are not followed: a
3xxanswer counts as a failure. - Answer within 10 seconds with any
2xx. The response body is ignored. Do the work after answering, for example by putting the event on a queue. - Each endpoint gets its own delivery (
wd_...) of each event it subscribes to.
Headers
RelationsAI-Signature: The signature; see Signature scheme.RelationsAI-Event-Id: The event id, the same asidin the body.RelationsAI-Event-Type: The event type, the same astypein the body.RelationsAI-Delivery-Id: The delivery id (wd_...), different for each endpoint and unchanged across retries.User-Agent:RelationsAI-Webhooks/1.0Content-Type:application/json
Signature scheme
Text
- Take
t(the time the signature was made, in Unix seconds) and eachv1value from the header. - Compute HMAC-SHA256 with the endpoint's signing secret as the key, over the string
<t>.<raw request body>: the exact bytes you received, before any JSON parsing. Hex-encode it. - Compare it with each
v1in constant time. Accept the event if any matches. - Reject the event if
tis more than 5 minutes from your server's clock, to stop replays.
During a secret rotation's grace period, the header carries two v1 values (never more), one per signing secret, so your receiver keeps verifying while you deploy the new secret. Code in three languages is in Verifying webhook signatures.
Retry schedule
A delivery fails when your server answers anything other than 2xx, takes longer than 10 seconds, redirects, or cannot be reached. Failed deliveries are retried after about:
| Attempt | Delay after the previous attempt |
|---|---|
| 2 | 10 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 6 hours |
| 8 | 12 hours |
After the last attempt the delivery is marked failed.
Back-pressure can stretch this schedule:
- At most 2 attempts are in flight to one endpoint at a time.
- After 5 failed attempts in a row, an endpoint is paused (for 1 minute, doubling up to 1 hour). Its waiting deliveries are attempted after the pause without using up their attempts, and one success resumes it. An endpoint is never disabled automatically.
- Live-mode deliveries are attempted before test-mode ones, and a workspace's test-mode deliveries are limited to 60 attempts a minute.
So a delivery to an endpoint that has been failing for a while can arrive later than the schedule above. Deliveries are kept for 30 days: list them with GET/webhook-endpoints/{webhook_endpoint_id}/deliveries (each shows its status, attempts, response_status and error) and send one again with POST/webhook-deliveries/{webhook_delivery_id}/retry.
Webhooks or polling
Webhooks are the recommended way to follow calls and campaigns: they arrive within seconds and do not use your rate limits. Keep a fallback: if an expected event has not arrived, read the object from the API (for example, a call that has not reported call.settled after an hour). See Tracking call progress.