> ## Documentation Index
> Fetch the complete documentation index at: https://platform-docs.sarj.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive a POST when a call ends — completed, no answer, or failed.

When a call reaches a terminal state, Sarj.ai POSTs a JSON payload to your configured webhook URL. You don't need to poll `getCall` — webhooks push the same data the moment it's available.

## Configure

Set your webhook URL in the [Sarj.ai Dashboard](https://platform.sarj.ai) (one URL per organization). The endpoint must:

* Be reachable over HTTPS (HTTP also works for staging, not recommended for production)
* Return 2xx within 10 seconds
* Be idempotent — see [Retries](#retries) below

## Payload

Every notification has the shape:

```json theme={null}
{
  "call_id": "call_8f9b2c1e-4a5d-4f6e-8b1a-2c3d4e5f6a7b",
  "payload": { /* one of the variants below */ }
}
```

`payload.type` discriminates the variant.

### Completed call (`type: "complete"`)

Fires when the call reached the customer and finished. Includes the full recording URL, transcript, and report.

```json theme={null}
{
  "call_id": "call_8f9b2c1e-4a5d-4f6e-8b1a-2c3d4e5f6a7b",
  "payload": {
    "type": "complete",
    "call_started": "2026-04-12T10:30:05Z",
    "retry_attempt_number": 1,
    "root_call_id": null,
    "next_retry_at": null,
    "call_data": {
      "call_id": "call_8f9b2c1e-…",
      "direction": "outbound",
      "phone_number": "+966512345678",
      "status": "completed",
      "total_duration": 47,
      "enhanced_transcript": {
        "transcript": { "messages": [ /* user + assistant turns */ ] },
        "errors": []
      },
      "report": { /* scenario-specific structured output */ },
      "permanent_recording_url": "https://platform-api.sarj.ai/api/v1/calls/call_abc123/recording",
      "created_at": "2026-04-12T10:30:00Z",
      "started_at": "2026-04-12T10:30:05Z",
      "ended_at": "2026-04-12T10:30:52Z"
    },
    "response_body": { /* optional, scenario response */ }
  }
}
```

<Tip>
  `permanent_recording_url` is a stable Sarj URL authenticated with the same API key as the call API. Resolve it when you need a fresh download redirect. Older webhook payloads can also include the deprecated, short-lived `recording_url`.
</Tip>

### Not-completed call

Fires when the call never reached the customer — every attempt reports its outcome, including busy, no-answer, and dial failures. `type` indicates why:

| `type` | Meaning |
| - | - |
| `no_answer` | Rang, no pickup |
| `user_rejected` | Customer hung up / rejected the call (busy lines also report here) |
| `user_unavailable` | Carrier returned unavailable (off, out of coverage) |
| `automation` | Hit an IVR / voicemail / non-human |
| `rejected_by_carrier` | The carrier refused the call |
| `invalid_number` | The number could not be dialed |
| `sip_trunk_failure` | Telephony trunk failure before ringing |
| `cancelled` | A scheduled call or pending retry was cancelled before dialing |
| `expired` | A scheduled call or pending retry expired before dialing |
| `failed` | Telephony failure with no SIP-level reason |

```json theme={null}
{
  "call_id": "call_8f9b2c1e-4a5d-4f6e-8b1a-2c3d4e5f6a7b",
  "payload": {
    "type": "no_answer",
    "retry_attempt_number": 1,
    "root_call_id": null,
    "next_retry_at": "2026-04-12T12:30:00Z"
  }
}
```

## Retry attempts

When retries are configured, each dial is its own call with its own webhook, linked by the attempt fields present on both payload variants:

| Field | Meaning |
| - | - |
| `retry_attempt_number` | Which dial this was (1, 2, 3, …) |
| `root_call_id` | The first attempt's `call_id`, shared by every retry in the group; `null` on the first attempt itself |
| `next_retry_at` | When the next attempt is booked to dial. **`null` means no more attempts are coming** — that webhook is the group's last word |

A typical retried contact looks like: attempt 1 `no_answer` with `next_retry_at` set, attempt 2 `user_rejected` with `next_retry_at` set, attempt 3 `complete` with `next_retry_at: null`.

## Retries

Sarj.ai makes up to **3 delivery attempts** per call, a few seconds apart, each with a **5-second timeout**. A `4xx` response other than `408` or `429` is treated as a permanent rejection and is not retried.

After delivery is exhausted or permanently rejected, the per-call webhook state is recorded server-side. You can re-fetch the call detail via [`GET /calls/{call_id}`](/api-reference/calls/fetch-sarjai-voice-call-details-by-call_id) at any time — the data is durable.

<Warning>
  Your endpoint must be **idempotent**. The same `call_id` may arrive more than once if your server's 2xx response is delayed or dropped. Deduplicate on `call_id`.
</Warning>

## Verifying authenticity

<Note>
  Webhook signature verification (HMAC) is not yet shipped. For now, lock down your webhook endpoint by IP allow-listing (contact support for the current source IP range) or by adding a hard-to-guess secret path component to the URL (e.g. `https://your-app.com/webhooks/sarj/c8f4a2b6e1d…`).
</Note>

## Testing

The dashboard offers a "Send test webhook" button to fire a synthetic `complete` payload at your URL — use this while standing up the integration. After that, place a real test call to verify end-to-end.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.