Skip to main content
Sarj.ai POSTs a notification to your configured webhook URL after each call attempt is processed. Use it to receive call outcomes without repeatedly polling getCall. Delivery is separate from dial retries: each dial has its own call_id and notification.

Configure and authenticate

Open Webhooks in the dashboard and set your receiver URL. It must accept POST with a JSON body and return 2xx within 5 seconds. For authenticated deliveries:
  1. Under Authenticate deliveries, choose Add a credential and select a stored credential.
  2. Keep Header Name as Authorization and the scheme prefix as Bearer, or choose the header and prefix your receiver expects. An empty prefix sends the bare secret.
  3. Use an HTTPS URL; credentials cannot be configured with HTTP.
  4. Save the webhook and use its test control to verify delivery.
Sarj reads the selected credential for every delivery, so credential updates apply to subsequent sends. If that credential is unavailable, Sarj records the failure and sends nothing. Redirects are not followed; use your receiver’s final URL.
This credential belongs to your receiver. It is separate from the Sarj API key used to call the platform. Webhook HMAC signatures are not currently available; validate the configured credential before accepting an event.

Payload and schema

Every event contains call_id and payload. Read payload.type to distinguish a started-call payload (complete) from a not-completed outcome. The public OpenAPI contract describes these outbound notifications under webhooks.callEnded. This is your receiver’s contract, not a new endpoint on the Sarj API.

Started call (type: "complete")

complete identifies the payload shape, not whether a human answered or the scenario succeeded. Inspect payload.call_data.status for the call’s outcome. In particular, voicemail uses type: "complete" with call_data.status: "voicemail"; mid-call voicemail does not produce type: "automation".
Download complete JSON · Download voicemail JSON call_started, call_data, duration, transcript, report, and recording fields can be null. A missing duration does not turn voicemail into a failed dial. A missing report or recording does not mean webhook delivery failed; the event may be the final available outcome, so do not poll indefinitely waiting for those fields.
Prefer permanent_recording_url when present: resolve it with your Sarj API key when you need a fresh recording download redirect. It is null when no recording exists or endpoint URLs are not enabled. Older payloads also carry the deprecated, expiring recording_url; keep that fallback during migration.

Not-completed call

An attempt that ended before the call started has one of these payload.type values:
Download no-answer JSON · Download cancelled JSON · Download expired JSON

Dial retries and delivery retries

When dial retries are configured, each attempt has its own call_id. Both payload variants include: Correlate attempts with root_call_id or the first attempt’s call_id. Deduplicate deliveries on each individual call_id, so a later dial is not mistaken for a duplicate. Sarj makes up to 3 delivery attempts for a notification, each with a 5-second request timeout. A 4xx other than 408 or 429 is a permanent rejection. An authenticated redirect is also permanent; no delivery follows redirects. Return 2xx only after the event is stored durably. Return 5xx when storage fails so a remaining delivery attempt can retry.

Receive and acknowledge

This example verifies Authorization: Bearer <receiver-secret>, commits a durable inbox row, and then returns 204. A unique call_id makes duplicate deliveries harmless. The synchronous handler runs in FastAPI’s thread pool, and SQLite waits at most one second for a database lock. Install FastAPI and Uvicorn, then save this as receiver.py:
receiver.py
Use a stored API key containing a secret your receiver owns for this example. Set SARJ_WEBHOOK_SECRET to that same secret, and set SARJ_WEBHOOK_DB_PATH to a database file on persistent storage. Start the receiver behind your HTTPS ingress:
Run a separate worker that reads unprocessed inbox rows, retries processing failures, and sets processed_at only after the business update commits. Keep processed rows long enough to deduplicate redeliveries. If processing calls another service, make that side effect idempotent using call_id as well. The SQLite example is for one persistent host. For multiple replicas, use a shared transactional inbox with the same unique key; an in-memory queue, background task, or ephemeral file does not make an acknowledgment durable. Apply your application’s access and retention controls to stored call data.

Test and recover

Use the dashboard’s webhook test control to check reachability and credential placement. It sends a synthetic complete payload with an all-zero call ID; it does not represent a real call. The downloadable fixtures above cover additional outcomes without dialing anyone. Verify that your receiver accepts an authorized fixture, rejects the wrong credential, acknowledges a duplicate without reprocessing it, and returns a failure when durable storage is unavailable. Then use a deliberate test call in your test environment to verify the full flow. If delivery is exhausted or permanently rejected, Sarj records the per-call delivery state. Retrieve durable call details with GET /calls/{call_id} to reconcile the outcome; do not assume another delivery will arrive automatically.
Last modified on October 26, 2026