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

# Webhooks

> Receive call lifecycle events as they happen

Voice Agents emit webhook events across a call's lifecycle. Subscribe to them on a sender webhook to log conversations, trigger follow-ups, or update your systems in real time.

Configure the events the same way as any other sender webhook — see [Webhooks](/guides/receiving-messages/webhooks) for setup, and [Security](/guides/receiving-messages/security) to verify signatures. Voice events are signed exactly like `message.*` events, with the `X-Zavu-Signature` header.

## Events

| Event            | When it fires                                                          | `data.status`                                |
| ---------------- | ---------------------------------------------------------------------- | -------------------------------------------- |
| `call.initiated` | An outbound call is dialing, or an inbound call is received.           | `ringing`                                    |
| `call.answered`  | The call is answered and the agent is connected.                       | `in_progress`                                |
| `call.completed` | The call ended after a conversation.                                   | `completed`                                  |
| `call.failed`    | The call could not be completed (busy, no answer, canceled, or error). | `busy`, `no_answer`, `canceled`, or `failed` |

Subscribe to `call.completed` and `call.failed` to handle outcomes. Add `call.initiated` and `call.answered` when you need live progress.

## Payload Structure

All voice events share the standard webhook envelope. The `data` object carries the call fields.

```json theme={null}
{
  "id": "evt_1705312200000_call123",
  "type": "call.completed",
  "timestamp": 1705312200000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": {
    "callId": "call_abc123",
    "direction": "inbound",
    "from": "+14155551234",
    "to": "+13125559876",
    "status": "completed",
    "durationSeconds": 84,
    "endReason": "agent_ended",
    "transcriptAvailable": true
  }
}
```

### Data Fields

| Field                 | Type           | Description                                                                                                                   |
| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `callId`              | string         | The call identifier. Use it to fetch the full call and transcript.                                                            |
| `direction`           | string         | `inbound` or `outbound`.                                                                                                      |
| `from`                | string         | Caller number in E.164 format.                                                                                                |
| `to`                  | string         | Callee number in E.164 format.                                                                                                |
| `status`              | string         | Call status for this event.                                                                                                   |
| `durationSeconds`     | number \| null | Connected talk time in seconds. `null` before the call is answered.                                                           |
| `endReason`           | string \| null | Why the call ended (e.g. `agent_ended`, `max_duration`, `transfer`, `hangup`). Present on `call.completed` and `call.failed`. |
| `transcriptAvailable` | boolean        | Whether a transcript can be fetched for this call.                                                                            |

## call.initiated

Fired when an outbound call starts dialing or an inbound call arrives.

```json theme={null}
{
  "id": "evt_1705312200000_init123",
  "type": "call.initiated",
  "timestamp": 1705312200000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": {
    "callId": "call_abc123",
    "direction": "outbound",
    "from": "+13125559876",
    "to": "+14155551234",
    "status": "ringing",
    "durationSeconds": null,
    "endReason": null,
    "transcriptAvailable": false
  }
}
```

## call.answered

Fired when the call connects and the agent begins the conversation.

```json theme={null}
{
  "id": "evt_1705312210000_ans123",
  "type": "call.answered",
  "timestamp": 1705312210000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": {
    "callId": "call_abc123",
    "direction": "outbound",
    "from": "+13125559876",
    "to": "+14155551234",
    "status": "in_progress",
    "durationSeconds": null,
    "endReason": null,
    "transcriptAvailable": false
  }
}
```

## call.completed

Fired when a call ends after a conversation. Fetch the call to read the transcript.

```json theme={null}
{
  "id": "evt_1705312300000_done123",
  "type": "call.completed",
  "timestamp": 1705312300000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": {
    "callId": "call_abc123",
    "direction": "outbound",
    "from": "+13125559876",
    "to": "+14155551234",
    "status": "completed",
    "durationSeconds": 92,
    "endReason": "agent_ended",
    "transcriptAvailable": true
  }
}
```

## call.failed

Fired when a call could not be completed. Check `status` and `endReason` for the cause.

```json theme={null}
{
  "id": "evt_1705312260000_fail123",
  "type": "call.failed",
  "timestamp": 1705312260000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": {
    "callId": "call_abc123",
    "direction": "outbound",
    "from": "+13125559876",
    "to": "+14155551234",
    "status": "no_answer",
    "durationSeconds": null,
    "endReason": "no_answer",
    "transcriptAvailable": false
  }
}
```

## Example Handler

Verify the signature, acknowledge quickly, and process asynchronously — the same pattern as message webhooks.

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function handleVoiceEvent(event) {
    const { callId, status, direction, durationSeconds } = event.data;

    switch (event.type) {
      case "call.completed": {
        // Fetch the transcript for records or analysis
        const { call } = await zavu.calls.retrieve(callId);
        await db.calls.save({
          callId,
          direction,
          durationSeconds,
          transcript: call.transcript,
        });
        break;
      }
      case "call.failed":
        await db.calls.markFailed(callId, event.data.endReason);
        break;
    }
  }
  ```

  ```python Python theme={null}
  async def handle_voice_event(event):
      data = event["data"]
      call_id = data["callId"]

      if event["type"] == "call.completed":
          # Fetch the transcript for records or analysis
          result = zavu.calls.retrieve(call_id)
          await db.calls.save(
              call_id=call_id,
              direction=data["direction"],
              duration_seconds=data["durationSeconds"],
              transcript=result.call.transcript,
          )
      elif event["type"] == "call.failed":
          await db.calls.mark_failed(call_id, data["endReason"])
  ```
</CodeGroup>

<Tip>
  Webhook deliveries may be retried, so make your handler idempotent — key it on the top-level event `id`. See [Event Types](/guides/receiving-messages/events#best-practices) for the pattern.
</Tip>

## Next Steps

<CardGroup cols={2}>
  <Card title="Security" icon="shield" href="/guides/receiving-messages/security">
    Verify webhook signatures
  </Card>

  <Card title="Configuration" icon="sliders" href="/guides/voice-agents/configuration">
    Tune how the agent behaves on calls
  </Card>
</CardGroup>
