Calls
Every call leaves a row, written at answer time rather than at the end — a call that drops mid-way is exactly the one worth investigating, and writing only on completion loses it.
List calls
GET /api/voice/calls?call_type=outbound&outcome=completed&limit=50
| Param | Notes |
|---|---|
call_type | inbound or outbound |
outcome | See outcomes |
sentiment | positive, neutral, negative |
flow_uuid | Restrict to one flow |
date_from / date_to | RFC 3339 or YYYY-MM-DD; a bare date_to covers the whole day |
search | Matches customer name, from_number, or to_number |
limit / offset | Default 50 / 0 |
{
"calls": [
{
"uuid": "cal-c1d2e3f4-a5b6-7890-abcd-ef1234567890",
"call_type": "outbound",
"from_number": "+915550010000",
"to_number": "+918888888888",
"duration": 113,
"duration_formatted": "1:53",
"tts_provider": "sarvam",
"outcome": "initiated",
"sentiment": null,
"summary": null,
"key_details": null,
"recording_url": "https://…/recording.mp3",
"started_at": "2026-09-20T14:32:01Z",
"ended_at": "2026-09-20T14:33:54Z",
"created_at": "2026-09-20T14:32:01Z",
"customer_name": "Priya Kumar",
"flow_name": "September winbacks",
"agent_name": "Aria"
}
],
"total": 891
}
customer_name, flow_name, and agent_name are joined from related rows and
use omitempty — when there is no linked customer, flow, or agent the key is
absent from the object entirely, not present as null. An inbound call
from an unknown number has no customer_name; a human click-to-call has no
agent_name unless one was recorded. Read them defensively.
from_number and to_number are literal call legs, not roles. On an
outbound call from_number is your own number and to_number is the
customer's; on an inbound call it is the other way round. A UI showing "the
customer" must branch on call_type.
Get a call
GET /api/voice/calls/{uuid}
Returns the full record including the transcript, which the list endpoint omits to keep pages small:
{
"uuid": "cal-c1d2e3f4-…",
"transcript": [
{ "speaker": "agent", "text": "Hi, is that Priya?", "ts": "2026-09-20T14:32:04Z" },
{ "speaker": "customer", "text": "Yes, speaking.", "ts": "2026-09-20T14:32:07Z" }
],
"config": { "mode": "outbound_campaign", "script_text": "Hi Priya, this is Aria…" }
}
Live calls
GET /api/voice/calls/live
Calls that have started and not yet ended:
{
"calls": [
{
"uuid": "cal-…", "call_type": "inbound",
"from_number": "+918888888888", "to_number": "+915550010000",
"started_at": "2026-09-20T14:32:01Z",
"customer_name": "Priya Kumar", "flow_name": "Support line"
}
]
}
Derived from the database rather than from in-memory state, so a call whose process died is still visible.
A call is listed here until its hangup webhook arrives. Ensure
PUBLIC_BASE_URL is reachable by your telephony provider so that calls are
closed out promptly.
Statistics
GET /api/voice/calls/statistics?flow_uuid=…&date_from=2026-09-01
{
"total_calls": 891,
"connected_calls": 543,
"calls_today": 42,
"avg_duration": 97,
"connect_rate": 0.61
}
"Connected" means duration > 0. These figures cover the whole
organization.
Export
GET /api/voice/calls/{uuid}/export
Streams the filtered list as CSV, capped at 10,000 rows and ignoring
pagination. It takes the same query parameters as the list endpoint; the
{uuid} in the path is not used to select a single call.
Content-Type: text/csv
Content-Disposition: attachment; filename="calls.csv"
Outcomes
| Value | Set when |
|---|---|
initiated | An outbound call row is created, before dialling |
completed | The provider reported a normal hangup |
no_answer | Nobody picked up, or the dial timed out |
busy | Busy signal |
failed | Dial failed, invalid or unallocated number |
rejected | The callee declined |
cancelled | The call was cancelled before connecting |
Human click-to-call adds agent_answered, agent_no_answer,
customer_no_answer.
Determining whether a call connected
outcome reflects what the telephony provider reported, and is most meaningful
for calls that never connected — a no-answer, a busy signal, a failed dial.
For a call handled by the AI pipeline, use ended_at and duration instead:
| Signal | Meaning |
|---|---|
duration > 0 | The call connected and audio was exchanged |
ended_at is set | The call is over |
ended_at is null | Still in progress |
An outbound campaign call carries initiated from the moment its row is
created, and an inbound AI call may report null, so neither value on its own
distinguishes a completed conversation from one that never connected.
sentiment, summary, and key_details are present on the record and are
populated by post-call analysis where that is configured.
Call endings
The agent ends the call when its own reply contains a closing phrase — checked after the reply has been spoken, so the farewell is not cut off mid-word.
The phrases are matched as substrings, case-insensitively:
have a good day, have a great day, thank you for calling, goodbye,
take care, dhanyavaad, shubh din.
Matching is on the whole reply rather than only its final sentence, so a closing phrase used mid-conversation also ends the call.
A configured greeting is spoken verbatim and does not pass through this
check. Without one, the model composes its own opening, and an opening
containing a closing phrase will end the call as soon as it is spoken. See
Flows.
A separate set of phrases triggers a handoff to a
human instead: connect you, put you through, transfer you,
someone who can help, a colleague will, specialist will.
Barge-in
The caller can talk over the agent. Speech detection cancels generation and flushes audio the provider has already buffered.
Interruption is detected while the agent's audio is being streamed to the
provider. VOICE_VAD_MIN_VOLUME (default 0.045) sets the detection floor —
lower it if callers report that talking over the agent has no effect, raise it
if background noise is cutting the agent off.
Recordings
Started over the provider's REST API once the media stream opens, and stopped explicitly when the call ends — an abandoned recording holds provider resources for the full hour and produces a file that is mostly silence.
recording_url is filled by the provider's recording callback, which can
arrive well after everything else about the call was written. A transferred
call keeps recording across the handover, so the file captures the human
conversation too.