Calls
Start a call
POST /api/customers/{id}/calls · scope calls:write
{
"agent_id": 12,
"kb_document_ids": [3, 7]
}
| Field | Required | Notes |
|---|---|---|
agent_id | ✓ | The teammate to ring first — an id, not a number |
kb_document_ids | — | Which documents suggestions may draw on |
agent_id is an id rather than a phone number on purpose: agents are org
members with a stored handset, so a call can only ever reach someone who
already has an account here.
kb_document_ids means no suggestionsIt does not mean "search everything". Omitting the field gives the rep a call with no assistance at all — which is a valid choice, but make it deliberately.
What happens
- The agent's phone rings
- When they answer, the customer is dialled
- Call audio is forked to
nextneural_assist, transcribed per leg - Suggestions stream to the rep over the agent-assist socket
| Status | Cause |
|---|---|
400 | {"detail": "choose which agent should take this call"} |
400 | {"detail": "that agent is inactive and can't take calls"} |
400 | {"detail": "this customer has no phone number and cannot be called"} |
400 | {"detail": "this customer's phone number isn't a valid number"} |
400 | {"detail": "one or more selected documents aren't available for AI — enable them in Knowledgebase first"} |
404 | {"detail": "that agent isn't set up in your organization"} |
404 | {"detail": "customer not found"} |
The knowledge base check happens before the call is placed — a rep should not discover mid-conversation that their documents were never vectorized. See Knowledge base.
Open to every role: placing a call is day-to-day sales work.
Live calls
GET /api/calls/live · scope calls:read
Every call currently in progress in the organization — a supervisor view.
{
"calls": [
{
"uuid": "7f3a1b2c-…",
"customer_id": 87,
"customer_name": "Priya Kumar",
"customer_company": "Pragati Interiors",
"agent_type": "human",
"call_type": "outbound",
"from_number": "+915550010000",
"to_number": "+918888888888",
"status": "in_progress",
"started_at": "2026-09-20T14:32:01Z",
"answered_at": "2026-09-20T14:32:09Z",
"ended_at": null
}
]
}
Note started_at and answered_at are distinct: the gap between them is how
long the phone rang.
My live call
GET /api/calls/live/mine · scope calls:read
{ "call": null }
The caller's own current call under a call key, null when they are not
on one. This is what a rep's panel polls
to know when to open the assist socket — it avoids showing them a colleague's
call, and avoids needing supervisor visibility to see your own.
Get a call
GET /api/calls/{uuid} · scope calls:read
The full record including transcript and recording, once the call has ended.
Call statuses
| Status | Meaning |
|---|---|
initiated | The agent's leg is ringing |
agent_answered | The rep picked up; the customer is being dialled |
in_progress | Both legs connected |
completed | Ended normally |
agent_no_answer | The rep never answered — the customer was not called |
customer_no_answer | The customer did not answer |
failed | The call could not be placed |
agent_no_answer is worth distinguishing in any reporting you build: the
customer was never disturbed, so it is not a failed outreach attempt.
Provider callbacks
Called by the provider, not by you:
| Endpoint | Purpose |
|---|---|
POST /api/webhooks/plivo/answer/{uuid} | A leg answered |
POST /api/webhooks/plivo/hangup/{uuid} | A leg ended |
POST /api/webhooks/plivo/recording/{uuid} | Recording ready |
POST /api/webhooks/plivo/stream-status/{uuid} | Media stream verdict |
GET/POST /api/webhooks/exotel/stream-url | Returns the socket address |
GET/POST /api/webhooks/exotel/connect-params | Returns connection parameters |
Both methods are accepted on the Exotel routes because providers differ in which they use.
Exotel answers a stream request it could not match to an agent with an
unmatched- address rather than refusing it, because the customer is already
on the line. Such a call has no row and nothing watching it — the audio is
still accepted, so the failure surfaces as an empty assist panel plus a log
line, rather than as a dropped call on a real customer.
Dashboard
GET /api/dashboard · scope calls:read
{
"window": "today",
"stats": {
"sessions_total": 0,
"sessions_active": 0,
"prompts_served": 0,
"avg_response_ms": null
},
"trend": [ { "date": "2026-09-16", "sessions": 0 } ]
}
Measures assist sessions and the prompts served to reps, not call volume —
avg_response_ms is how quickly suggestions reached the screen.