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 for setup, and 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.
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;
}
}