# 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](https://www.zavu.dev/docs/guides/receiving-messages/webhooks) for setup, and [Security](https://www.zavu.dev/docs/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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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.

```typescript
async function handleVoiceEvent(event) {
  const { callId, status, direction, durationSeconds } = event.data;

  switch (event.type) {
    case "call.completed": {
      // Fetch the transcript for records or analysis. The voice endpoints are
      // not in the SDK yet, so call the API directly.
      const res = await fetch(`https://api.zavu.dev/v1/calls/${callId}`, {
        headers: { Authorization: `Bearer ${process.env.ZAVUDEV_API_KEY}` },
      });
      const { call } = await res.json();
      await db.calls.save({
        callId,
        direction,
        durationSeconds,
        transcript: call.transcript,
      });
      break;
    }
    case "call.failed":
      await db.calls.markFailed(callId, event.data.endReason);
      break;
  }
}
```

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

## Next Steps

  - [Security](https://www.zavu.dev/docs/guides/receiving-messages/security): Verify webhook signatures
  - [Configuration](https://www.zavu.dev/docs/guides/voice-agents/configuration): Tune how the agent behaves on calls
