Calls¶
The call log covers every phone call, web or widget session, and silent
agent_task run an agent has ever recorded, along with its full
turn-by-turn transcript. It also includes an endpoint for placing a manual
outbound call for testing, without a real event or
Trigger behind it.
Endpoint family¶
GET /api/v1/agents/{agent_id}/calls
GET /api/v1/agents/{agent_id}/calls/{call_id}
GET /api/v1/agents/{agent_id}/calls/{call_id}/turns
POST /api/v1/agents/{agent_id}/calls
POST /api/v1/agents/{agent_id}/calls/{call_id}/monitor
POST /api/v1/agents/{agent_id}/calls/{call_id}/take-over
Authentication and roles¶
Every endpoint here takes an admin session token (see Authentication), not an API key.
| Endpoint | Required role |
|---|---|
GET (list, detail, turns) |
Owner, Admin, Editor, or Viewer |
POST (place a manual call) |
Owner or Admin |
POST .../monitor (listen in) |
Owner, Admin, Editor, or Viewer |
POST .../take-over |
Owner or Admin |
Reading the call log and transcripts is available to every admin role, including Viewer, since that's the read-only observability this role exists for. Placing a call is a live action with real Twilio cost behind it, so it's held to the same higher bar as managing API keys and other admins. An Editor cannot use this endpoint, even though Editors can create the Triggers that place calls indirectly.
List calls¶
Returns every call on the agent, newest first (ordered by id
descending), with no pagination and no filters.
[
{
"id": 456,
"agent_id": 1,
"session_id": "a1b2c3d4e5f6...",
"direction": "outbound",
"channel": "phone",
"status": "completed",
"phone_number": "+15557654321",
"from_number": "+15551112222",
"visitor_name": null,
"visitor_email": null,
"twilio_call_sid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"triggered_by_event_id": 123,
"triggered_by_trigger_id": 4,
"summary": "Caller confirmed they still want the items in cart 89 and asked for a discount code.",
"error_message": null,
"sentiment": "positive",
"outcome_tag": "resolved",
"started_at": "2026-01-15T10:30:05Z",
"ended_at": "2026-01-15T10:32:47Z",
"duration_seconds": 162,
"created_at": "2026-01-15T10:30:00Z"
}
]
Get a single call¶
Returns 404 if call_id doesn't exist, or belongs to a different agent
than the one in the URL.
Fields¶
| Field | Type | Description |
|---|---|---|
id |
integer | The call's id. |
agent_id |
integer | The agent this call belongs to. |
session_id |
string | An opaque session identifier generated when the call was created. |
direction |
string | inbound or outbound. |
channel |
string | web (widget), phone, or agent_task (a headless, silent run started by an agent_task Trigger, see Triggers, no audio, no human on the line). |
status |
string | pending, ringing, in_progress, completed, failed, no_answer, busy, or transferred (handed off to a human via the agent's transfer_to_human tool). |
phone_number |
string or null | The recipient's number for a phone-channel call; null for web and agent_task. |
from_number |
string or null | The Twilio number the call placed from. |
visitor_name |
string or null | Set for a web-channel call if the widget visitor supplied a name. |
visitor_email |
string or null | Same, for email. |
twilio_call_sid |
string or null | Twilio's own call identifier, useful for cross-referencing the Twilio console or API directly. |
caller_line_type |
string or null | The number's line type (mobile, landline, nonFixedVoip, ...) from Twilio Lookup, when Lookup screening is enabled; null otherwise. |
answered_by |
string or null | Twilio's Answering Machine Detection verdict for an outbound call (human, machine_end_beep, fax, ...), when the agent has detection enabled; null otherwise. A machine_* or fax value pairs with status no_answer and error_message "reached voicemail". |
quality |
object or null | Twilio Voice Insights audio-quality summary (quality_score, mos_avg, jitter_avg, packet_loss_pct), present a couple of minutes after a phone call ends when Voice Insights fetching is enabled. |
triggered_by_event_id |
integer or null | The event that queued this call, if any (null for a manual call placed via POST below, or an inbound call). |
triggered_by_trigger_id |
integer or null | The Trigger that queued this call, if any. |
summary |
string or null | An AI-generated summary of the conversation, written after the call ends. |
error_message |
string or null | Set when status is failed. |
sentiment |
string or null | positive, neutral, or negative, classified after the call ends; null until classified (or forever, for a call too short to judge). See Analytics. |
outcome_tag |
string or null | A short free-text outcome (e.g. "resolved", "booked appointment"), classified alongside sentiment. |
started_at |
datetime or null | When the call actually connected; null if it never got past pending/ringing. |
ended_at |
datetime or null | When the call ended. |
duration_seconds |
integer or null | Connected duration. |
created_at |
datetime | When the Call record was created (before dialing, for an outbound call). |
Get a call's transcript¶
Returns every Turn in the conversation, ordered by sequence.
The transcript is written live, not reconstructed afterward
Each Turn is saved as the conversation happens, one row per
utterance (buffered by speaker and flushed on a role change or a
turn-complete signal from the model), not assembled from an
end-of-call summary. That means you can poll this endpoint against an
in_progress call and see turns accumulate in near real time,
without waiting for status to reach completed. The call's
summary field is a separate, AI-generated artifact written only
once the call ends; the turn list is the actual record.
[
{
"id": 9001,
"role": "agent",
"text": "Hi Prince, I'm calling about your cart worth $89. Is now a good time?",
"tool_name": null,
"tool_args": null,
"tool_result": null,
"sequence": 0,
"created_at": "2026-01-15T10:30:06Z"
},
{
"id": 9002,
"role": "user",
"text": "Sure, go ahead.",
"tool_name": null,
"tool_args": null,
"tool_result": null,
"sequence": 1,
"created_at": "2026-01-15T10:30:11Z"
},
{
"id": 9003,
"role": "tool",
"text": null,
"tool_name": "lookup_discount_code",
"tool_args": { "customer_id": "cust_881" },
"tool_result": "SAVE10",
"sequence": 2,
"created_at": "2026-01-15T10:30:22Z"
}
]
Fields¶
| Field | Type | Description |
|---|---|---|
id |
integer | The turn's id. |
role |
string | user, agent, system, or tool. |
text |
string or null | The spoken/written content, for user/agent/system turns; typically null for a tool turn. |
tool_name |
string or null | The tool called, for a tool turn. |
tool_args |
object or null | The arguments the model passed to the tool. |
tool_result |
string or null | The tool's return value, as text. |
sequence |
integer | The turn's position in the conversation; this is the sort key, not id or created_at. |
created_at |
datetime | When this turn was persisted. |
Place a manual outbound call¶
Queues an outbound call immediately, bypassing event ingestion and Trigger matching entirely, but going through the exact same durable call queue an event-triggered call uses. Use this to test an agent's phone setup (voice, greeting, tools) end to end without wiring up a real event source or Trigger first.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
phone_number |
string | Yes | The number to call, 1-20 characters. |
context |
string or null | No | Static dynamic-context text for the agent's opening, the manual-call equivalent of a Trigger's rendered context_template. No Jinja2 rendering happens here, it's used verbatim. |
const response = await fetch("https://api.your-domain.com/api/v1/agents/1/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
phone_number: "+15557654321",
context: "This is a manual test call.",
}),
});
const call = await response.json();
Returns 201 Created with the new Call record (direction: "outbound",
channel: "phone", status: "pending", triggered_by_event_id and
triggered_by_trigger_id both null). Poll
GET /api/v1/agents/1/calls/{id} or its /turns to watch it progress.
Returns 422 Unprocessable Entity if the agent has no
twilio_phone_number configured. Without one, there's no number for
AiFlow to place the call from, so it can't be queued at all.
Listen in on a live call¶
Starts a listen-in session on an in_progress, phone-channel call: a
second, independent audio stream is forked from the live call (without
ending its Gemini Live session) and relayed to the admin dashboard over
its own WebSocket. It's available to any admin role, the same read-only
bar as viewing a transcript.
Live monitoring is licensed. On a plan without it this route returns
404, like every other unlicensed surface. Taking over a call is gated
separately, on warm transfer, since that is what it actually does and it
does not require having listened in first.
The returned token is a short-lived (5-minute), single-purpose
credential for wss://api.your-domain.com/api/v1/twilio/monitor-listen
?call_id={call_id}&token={token}. Connect there and the socket delivers
raw 16-bit PCM audio frames (8kHz, mono) as binary WebSocket messages
until you disconnect or the call ends. There's no REST call to stop
listening: closing the WebSocket is enough.
Returns 422 Unprocessable Entity if the call isn't a phone-channel
call currently in_progress, or 502 Bad Gateway if Twilio rejects the
request to fork the stream.
Take over a call¶
Redirects a live, in_progress phone call to a human, ending the AI's
turn immediately. It's the same conference-based handoff the agent's own
transfer_to_human tool uses, just triggered by an admin (for example,
one who was listening in) instead of the AI deciding to transfer.
Requires Owner or Admin, the same bar as placing a manual call.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
phone_number |
string | Yes | The number to redirect the call to, 1-20 characters. |
reason |
string or null | No | A short note; defaults to a generic "admin took over" message. |
Returns 204 No Content on success, and the call's status becomes
transferred. Returns 422 Unprocessable Entity if the call isn't a
phone-channel call currently in_progress, or 502 Bad Gateway if
Twilio rejects the transfer.
Next¶
- Triggers: the rules that queue calls automatically from real traffic, instead of the manual path above.
- Event log: find the event behind a
triggered_by_event_id. - Webhook subscriptions: get notified
asynchronously (
call.finishedorcall.failed) instead of polling this page.