Webhooks
Webhook events
This page lists every event Vaakyo 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.
Lifecycle events and timeline events
By default (the agent’s webhook URL, and endpoints subscribed to "*") you get the call’s lifecycle: call.queued → call.placed (dialling) → call.started (in progress) → call.ended → call.completed (the final record, after analytics). An endpoint can also list, by name in events, any of call.voicemail, call.rescheduled, call.transferred, call.transfer_status, tool.call, error and guardrail.violation (GET /api/v1/webhooks/events returns the list). The other events below are on the call’s timeline (GET /api/v1/calls/{id}/events) but are not sent to endpoints.
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 (data.provider is plivo). | |
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 |
kb.search | The agent searched its knowledge bases. latency_ms is the whole search; level is warning if it failed. | query, hits, embed_ms, search_ms, passages, top (source, meaning score and keyword match of each passage found), error if it failed |
kb.inline | The agent’s knowledge bases are small enough and were put whole into its prompt. | characters |
user.online_check | The agent asked whether the caller is still there. | |
hangup.prompt | The hang-up prompt decided the conversation is over. | |
fallback.used | A fallback voice or transcriber took over for the rest of the call. level is warning. | component (tts or stt), reason, from, to |
capacity.fallback | The voice’s or transcriber’s provider was at its concurrency limit, so a fallback voice spoke that reply, or a fallback transcriber took the call. Once per call for each component and provider. level is warning. | component (tts or stt), provider (the full one), from, to |
capacity.exceeded | The provider was at its concurrency limit and no fallback had room, so the call went ahead on it anyway. level is warning. | component, provider, from |
call.dtmf | The caller pressed phone keys (keypad input on); message is what the agent received, e.g. [keypad: 1 2 3 #]. | digits (e.g. "123#") |
call.voicemail | An answering machine picked up (voicemail detection on): the agent leaves its voicemail message, or hangs up. | answered_by (the carrier’s verdict, e.g. machine_end_beep), message (true when a message is left) |
error | Something went wrong. level is warning or error. | Sometimes provider |
call.rescheduled | A call back was booked (auto reschedule); message gives the local time. level is warning when the after-call check found a time it could not book. | call_id (the booked call), at (UTC), source (tool or post_call), note |
call.transferred | The agent handed the caller to a person (transfer to a human); the agent’s part ends next. | name, number, reason |
guardrail.violation | After the call, the platform guardrail check found that the agent broke a rule (Guardrails). One event per incident. level is error for a violation, warning for a warning. | incident_id, rule_id, rule_name, severity, quote, confidence, action (notice, frozen or warning), strike |
call.transfer_status | The transfer’s outcome: the person answered, or no one did. level is warning unless answered. | status (answered, no-answer, busy, failed or canceled), name, number, plivo (the carrier’s dial status, when not answered) |
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 |
|---|---|---|---|
| The carrier reported no answer, busy, failed or canceled | not connected: no-answer | warning | plivo: the carrier’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 Vaakyo"}
}
call.voicemail
The carrier’s answering-machine detection heard a machine. The call then ends with hangup_reason voicemail and the call’s answered_by is machine.
{
"type": "call.voicemail",
"data": {
"message": "answering machine: leaving the voicemail message",
"level": "info",
"latency_ms": null,
"answered_by": "machine_end_beep",
"message": true
}
}
Plivo reports only that a machine answered (answered_by is machine), not what kind.
fallback.used
The agent’s voice or transcriber failed and a backup took over. from and to name the provider, model, and the voice or language.
{
"type": "fallback.used",
"data": {
"message": "voice switched to cartesia/sonic-3 (Meera): voice not found",
"level": "warning",
"latency_ms": null,
"component": "tts",
"reason": "voice not found",
"from": {"provider": "cartesia", "model": "sonic-3.6", "voice_id": "a0e99841-438c-4a64-b679-ae501e7d6091", "voice_name": "Riya", "language": "hi"},
"to": {"provider": "cartesia", "model": "sonic-3", "voice_id": "f9836c6e-a0bd-460e-9d3c-f7299fa60f94", "voice_name": "Meera", "language": "hi"}
}
}
A backup that fails to connect at the start of the call has a reason starting with could not connect.
capacity.fallback
Speech providers limit how many streams and replies run at once. When the voice’s provider is full, the next fallback voice from another provider with room speaks that one reply (later replies go back to the main voice once it has room); when the transcriber’s provider is full as the call starts, the first fallback transcriber with room takes the call. Add fallback voices and transcribers from other providers so a busy provider never slows a call.
{
"type": "capacity.fallback",
"data": {
"message": "cartesia voice capacity full: this reply uses elevenlabs/eleven_flash_v2_5 (Anika)",
"level": "warning",
"latency_ms": null,
"component": "tts",
"provider": "cartesia",
"from": {"provider": "cartesia", "model": "sonic-3.6", "voice_id": "a0e99841-438c-4a64-b679-ae501e7d6091", "voice_name": "Riya", "language": "hi"},
"to": {"provider": "elevenlabs", "model": "eleven_flash_v2_5", "voice_id": "zT03pEAEi0VHKciJODfn", "voice_name": "Anika", "language": "hi"}
}
}
With no fallback that has room, a reply waits up to 1.5 seconds and is then spoken by the main voice anyway (capacity.exceeded): the call is never left silent.