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 acceptPOST with a JSON body and return 2xx within 5 seconds.
For authenticated deliveries:
- Under Authenticate deliveries, choose Add a credential and select a stored credential.
- Keep Header Name as
Authorizationand the scheme prefix asBearer, or choose the header and prefix your receiver expects. An empty prefix sends the bare secret. - Use an HTTPS URL; credentials cannot be configured with HTTP.
- Save the webhook and use its test control to verify delivery.
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 containscall_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".
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.
Not-completed call
An attempt that ended before the call started has one of thesepayload.type values:
Dial retries and delivery retries
When dial retries are configured, each attempt has its owncall_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 verifiesAuthorization: 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
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:
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 syntheticcomplete 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.
