Guides

Webhooks

Receive every status change of a call, with custom headers, a status filter, retries and a dead-letter list.

A webhook is an HTTP POST the platform sends to your server each time a call changes status. The body is the execution as it is at that moment. This page covers call webhooks; workflows have their own, described in Campaigns and workflows.

In the dashboard: the webhook card in Agent Studio, the Webhooks tab of a call in Call History, and Developers → Jobs for failed deliveries.

Set the URL

The URL is part of the agent config.

bash
curl -X PATCH https://api.sevennadi.si/v2/agent/3c90c3cc-0d44-4b50-8822-8dd25736052a \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"agent_config": {
        "webhook_url": "https://hooks.example.test/calls",
        "webhook_headers": {"X-Api-Key": "<your secret>"},
        "webhook_statuses": ["completed", "no-answer", "busy", "failed"]
      }}'

A single call can use a different URL with agent_data.webhook_url, and a batch with its webhook_url form field. Those override the agent's URL for that call.

Headers

webhook_headers are sent with every delivery to the agent's own webhook_url. Use them to authenticate the platform to your server.

  • At most 20 headers. Values are strings on one line, up to 2,000 characters.
  • Reserved names are refused: Host, Content-Length, Content-Type, Transfer-Encoding, Connection, and anything starting with x-internal.
  • Values are encrypted at rest and never returned. Reading the agent shows {"X-Api-Key": "**********"}.
  • Headers are sent only to the agent's own URL, never to a per-call or batch URL.

Updating them:

You sendResult
No webhook_headers keyStored headers are kept
A value of **********That header's stored value is kept, so read-then-write is safe
An object without a headerThat header is removed
{} or nullAll headers are cleared

If webhook_url moves to a different scheme, host or port, stored headers are dropped unless you send the real values again. Headers without a URL are refused: webhook_headers need a webhook_url.

Status filter

webhook_statuses lists the statuses that produce a webhook. Empty or absent means every status. The allowed values are every call status: scheduled, queued, rescheduled, initiated, ringing, in-progress, call-disconnected, completed, no-answer, busy, failed, canceled, stopped, error, balance-low. GET /user/webhook/allowed-states returns the list.

The filter applies to every delivery for the agent's calls, including per-call and batch URLs.

Payload

The body is the same JSON as GET /executions/{execution_id}: id, agent_id, status, transcript, telephony_data, cost_breakdown, extracted_data, tool_call_logs and the rest. See Post-call data.

json
{
  "id": "b8f0c9a2-3e5d-4c1b-9f7a-2d6e8c4b1a90",
  "agent_id": "3c90c3cc-0d44-4b50-8822-8dd25736052a",
  "status": "ringing",
  "conversation_duration": 0.0,
  "total_cost": 0.0,
  "telephony_data": {"to_number": "+919876500123", "provider": "twilio", "call_type": "outbound"},
  "user_number": "+919876500123",
  "created_at": "2026-10-13T09:30:00.000000Z",
  "updated_at": "2026-10-13T09:30:03.000000Z"
}

Only the completed payload carries the final transcript, cost, summary and extraction. Earlier payloads are snapshots.

Answer with any 2xx status within the timeout (10 seconds by default). Do your own work after answering.

Ordering and retries

Deliveries go through an outbox, so a status is never lost if your server is briefly down.

  • Order: for one call, statuses are delivered oldest first. If one delivery fails, later statuses of the same call wait behind it.
  • Retries: a delivery that gets a non-2xx answer, times out or cannot connect is retried after a growing wait: 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2, 4, 8, 12, then 24 hours, each with up to 20% random jitter. A server that is down for a day still receives its statuses.
  • Limit: ten attempts by default; the nine waits between them add up to about 28 hours. After that the delivery is dead: it stops blocking the statuses behind it and appears in the dead-letter list.
  • Duplicates: a status is normally delivered once. If your server processed a request but answered too slowly, the retry will arrive again. Deduplicate on id plus status.

The timeout and attempt limit are operator settings.

Private-network URLs are blocked

Every delivery resolves the URL first. If it points at a private, loopback, link-local or otherwise internal address, it is not sent and not retried; the delivery goes straight to the dead-letter list with an error starting blocked:. The same check applies to the test endpoint, which answers 400 webhook_url is not allowed: ....

Send a test

bash
curl -X POST "https://api.sevennadi.si/v2/agent/3c90c3cc-0d44-4b50-8822-8dd25736052a/webhook:test" \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'
json
{
  "webhook_url": "https://hooks.example.test/calls",
  "status": "completed",
  "headers_sent": ["X-Api-Key"],
  "payload": {"id": "...", "status": "completed", "is_test": true},
  "result": {"delivered": true, "http_status": 200, "response_body": "ok"}
}

The payload is a made-up execution marked "is_test": true. All body fields are optional: send webhook_url and webhook_headers to try values you have not saved yet. A failing endpoint still gives 200, with result.delivered: false, an error_type of http_error or unreachable, and error. With no URL anywhere: 400 The agent has no webhook_url: save one or send webhook_url.

See what was delivered

bash
curl https://api.sevennadi.si/executions/b8f0c9a2-3e5d-4c1b-9f7a-2d6e8c4b1a90/webhooks \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"
json
{"data": [
  {"id": 4812, "webhook_url": "https://hooks.example.test/calls", "status": "queued", "attempts": 1,
   "response_code": 200, "delivered": true, "delivered_at": "2026-10-13T09:30:00.412000Z", "error": null,
   "created_at": "2026-10-13T09:30:00.101000Z"}
]}

Resend

POST /executions/{execution_id}/webhook:resend queues the execution's current payload again and answers 202:

json
{"execution_id": "b8f0c9a2-3e5d-4c1b-9f7a-2d6e8c4b1a90", "webhook_url": "https://hooks.example.test/calls",
 "status": "completed", "delivery_id": 4830}

The status filter is ignored for a resend. With no URL configured: 409 No webhook is configured for this execution's agent.

Dead-letter list

GET /webhooks/dead lists deliveries that used up their attempts or were blocked, newest first (limit 1 to 200, default 50):

json
{"total": 1, "deliveries": [
  {"id": 4799, "execution_id": "b8f0c9a2-3e5d-4c1b-9f7a-2d6e8c4b1a90", "webhook_url": "https://hooks.example.test/calls",
   "status": "completed", "attempts": 5, "response_code": 503, "error": "HTTP 503", "created_at": "2026-10-13T09:31:05.000000Z"}
]}

POST /webhooks/dead/{delivery_id}/resend gives one a fresh set of attempts with the same payload and URL, and answers 202.

Testing, resending and the dead-letter list need the admin role or an API key.

Limitations

  • Payloads are not signed. Authenticate deliveries with a secret header and serve your endpoint over https.
  • Custom headers are not sent to per-call or batch webhook URLs.
  • If the operator changes the platform's encryption key without rotating stored secrets, saved headers are lost; webhooks still go out without them.
  • Workflow webhook deliveries have no dead-letter view.