Webhooks
Webhook events
This page lists every event Voxa sends during a call, when it is sent, and the fields it carries in data.
Each event is also saved on the call’s timeline (GET /api/v1/calls/{id}/events), with the same type, message, level, latency_ms and data.
Order of events
A typical outbound call:
call.queued → call.dequeued → call.placed → call.started → welcome → tts.first_audio
→ user.turn → llm.first_token → tts.first_audio → agent.reply
→ user.turn → llm.first_token → tool.call → tool.result → tts.first_audio → agent.reply
→ ... → tool.call (end_call) → call.ended → call.completed
Inbound and browser calls start at call.started. A call that never connects ends with call.ended and has no call.completed.
All events
| Event | When | Extra data fields |
|---|---|---|
call.queued | An outbound call joined the queue. | position, by |
call.dequeued | A slot was free and the call is about to be dialled. latency_ms is how long it waited. | |
call.placed | Plivo accepted the dial request. | |
call.started | The call connected and the agent is live. | channel, from, to, stt, llm, tts, started_by, values |
welcome | The agent starts saying its welcome message (the text is in message). | |
tts.first_audio | The agent’s first audio of a reply played. latency_ms is from the caller finishing to this moment. | welcome: true for the welcome message |
user.turn | The caller finished a turn (message is what they said). | after_goodbye: true if said after the agent decided to hang up |
llm.first_token | The LLM started answering. latency_ms is from the caller finishing. | model_ms: time since the LLM request was sent |
agent.reply | The agent finished a reply (message is the full text). | |
interrupted | The caller talked over the agent, which stopped (message is what the caller heard). | |
tool.call | The agent called a tool. | name, args, url (name only for end_call) |
tool.result | The tool answered or failed. latency_ms is the request time. level is warning unless the status is 2xx. | name, status, result |
user.online_check | The agent asked whether the caller is still there. | |
hangup.prompt | The hang-up prompt decided the conversation is over. | |
error | Something went wrong. level is warning or error. | Sometimes provider |
call.ended | The call ended, for any reason. | see below |
call.completed | The final call record is saved, after post-call analytics. | call |
ping | A test from POST /api/v1/webhooks/{id}/test. |
call.started
{
"type": "call.started",
"data": {
"message": "outbound call with Appointment reminder",
"level": "info",
"latency_ms": null,
"channel": "phone",
"from": "+918035001234",
"to": "+919812345678",
"stt": "cartesia/ink-2",
"llm": "gemini-3.5-flash-lite",
"tts": "sonic-3.6/Riya",
"started_by": "",
"values": ["customer_name", "slot"]
}
}
values lists the user_data keys the call has (not their values). started_by is the person’s name for browser calls.
tool.result
{
"type": "tool.result",
"data": {
"message": "check_availability -> HTTP 200",
"level": "info",
"latency_ms": 184,
"name": "check_availability",
"status": 200,
"result": {"status": 200, "result": {"free_slots": ["10:30", "12:00"]}}
}
}
result is what the LLM received (see Tools), cut to about 3000 characters. status is 0 when your endpoint did not answer.
call.ended
When a call that connected ends:
{
"type": "call.ended",
"data": {
"message": "ended by agent: agent ended the call",
"level": "info",
"latency_ms": null,
"duration_seconds": 14.2,
"avg_first_audio_ms": 1050,
"usage": {"stt_seconds": 14.0, "tts_characters": 121, "llm_input_tokens": 1840, "llm_output_tokens": 41, "llm_calls": 1}
}
}
level is error when the call ended because of an error.
When a call never connected, message says why and there is no usage:
| Case | message | level | Extra |
|---|---|---|---|
| Plivo reported no answer, busy, failed or canceled | not connected: no-answer | warning | plivo: Plivo’s status |
| Canceled while queued | canceled while queued | info | |
| Expired, out of credits, agent deleted, dial refused | the hangup_reason | warning (error for credits and refusals) |
call.completed
Sent once per connected call, after post-call analytics, with the full call object plus transcript_text:
{
"id": "evt_1f2e3d4c5b6a79880a1b2c3d4e5f6a7b",
"type": "call.completed",
"created_at": "2026-10-01T09:14:21.733000+00:00",
"workspace_id": "6df74233a1b04c2e9f8d7c6b5a4e3d2c",
"call_id": "00b1d622a8f04f4f8f3a3c2d9e5b7a11",
"agent_id": "1b81a241c0e44f5c9d1f0e2a7c3b9d10",
"data": {
"message": "call completed",
"level": "info",
"latency_ms": null,
"call": {
"id": "00b1d622a8f04f4f8f3a3c2d9e5b7a11",
"agent_name": "Appointment reminder",
"direction": "outbound",
"to_number": "+919812345678",
"status": "completed",
"user_data": {"customer_name": "Rohan", "slot": "11 am"},
"transcript": [{"role": "assistant", "text": "Hello Rohan, ...", "at": "2026-10-01T09:14:02.410000Z", "interrupted": false}],
"transcript_text": "assistant: Hello Rohan, ...\nuser: Yes, I'll be there at eleven.\nassistant: Great, we'll see you tomorrow at 11 am. Goodbye!",
"duration_seconds": 14.2,
"cost_paise": 95,
"hangup_by": "agent",
"hangup_reason": "agent ended the call",
"summary": "City Clinic called Rohan to confirm tomorrow's 11 am appointment. ...",
"extracted": {"confirmed": true, "new_day": null},
"...": "..."
}
}
}
Times in webhooks are in UTC. This is the event to use for syncing results to your CRM: it has the outcome, the transcript, the analytics and the cost in one place.
ping
{
"id": "evt_ping",
"type": "ping",
"created_at": "2026-10-01T09:30:00.000000+00:00",
"workspace_id": "6df74233a1b04c2e9f8d7c6b5a4e3d2c",
"call_id": "",
"agent_id": "",
"data": {"message": "Test event from Voxa"}
}