Skip to content
RelationsAI
API fundamentals

Webhooks

Preview

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 same Idempotency-Key returns the endpoint with secret: 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: 0 is 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_hours above 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 enabled or disabled; 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.webhooks in GET/workspace says whether it is on. While it is off, endpoints cannot be created, test events and retries are refused with webhooks_not_enabled, and no events are sent.

Events

Every delivery is a POST with one event as its JSON body:

JSONJSON
{ "id": "evt_5Zx9Cv3Bn7Mq2Wl8Ek4Rt6", "object": "event", "type": "call.ended", "created_at": "2026-10-21T09:32:15.000Z", "livemode": true, "api_version": "2026-10-21", "data": { "object": { "id": "call_2Hd8Ks4Lq7Mw3Nx9Pz5Rc1", "object": "call", "status": "ended", "outcome": "completed", "settled": false } } }

(The call object is shortened here; events carry the full object.)

FieldMeaning
idEvent id (evt_...). The same event can be delivered more than once: use it to discard duplicates.
typeThe event type.
created_atWhen the event happened.
livemodeThe event's mode.
api_versionThe payload format version, today 2026-10-21.
data.objectA 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:

TypeSent when
call.endedA 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.settledEverything about a call is final, including transcript, recording, analysis and the charge.
campaign.startedA campaign started a run and began dialling.
campaign.pausedA campaign was paused, by a request or automatically (budget, balance, telephony).
campaign.resumedA paused campaign resumed its run.
campaign.stoppedA campaign was stopped.
campaign.completedA campaign run finished dialling every recipient.
campaign.failedA campaign run ended in failure before finishing.
webhook_endpoint.testA 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 the RelationsAI-Event-Id header).
  • 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 3xx answer 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 as id in the body.
  • RelationsAI-Event-Type: The event type, the same as type in the body.
  • RelationsAI-Delivery-Id: The delivery id (wd_...), different for each endpoint and unchanged across retries.
  • User-Agent: RelationsAI-Webhooks/1.0
  • Content-Type: application/json

Signature scheme

Text
RelationsAI-Signature: t=1792575135,v1=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
  1. Take t (the time the signature was made, in Unix seconds) and each v1 value from the header.
  2. 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.
  3. Compare it with each v1 in constant time. Accept the event if any matches.
  4. Reject the event if t is 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:

AttemptDelay after the previous attempt
210 seconds
31 minute
45 minutes
530 minutes
62 hours
76 hours
812 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.