List calls
GET /api/v1/calls — paginated, filterable list of every call placed in your organization.
GET /api/v1/callsPaginated list of calls. Every call placed via phone, WebSocket, or WebRTC lands here. Use the filters to narrow by transport, status, agent, time window, source, or tag.
Authentication
X-API-Key: zsk_...Query parameters
Pagination & sort
| Param | Type | Default | Notes |
|---|---|---|---|
page | int (≥1) | 1 | 1-indexed page number. |
per_page | int (1..100) | 50 | Items per page; capped at 100. |
sort_by | enum | ended_at | One of ended_at, created_at, duration_seconds, cost. |
sort_order | asc / desc | desc |
ended_at sort puts NULLs last so in-flight calls don't crowd the top of the default view.
Filters
| Param | Type | Description |
|---|---|---|
call_id | string | Exact match on callId. |
transport | csv | One or more of outbound, inbound, webrtc, websocket, web (comma-separated). Browser WebRTC calls are stored as webrtc (web also appears on some rows). Telephony inbound/outbound use the named values directly. |
status | csv | pending,active,completed,error — any combination. |
connection_status | csv | Filter on the connection dimension — did the call connect, and if not, why. Values: completed, no_answer, busy, rejected, invalid_number, unreachable, cancelled, connection_failed, no_participant, dial_failed, initiation_failed, missing_credentials, concurrency_limit_reached, insufficient_balance, the granular rejection values, and telephony_<raw> passthroughs. (See connectionStatus values.) |
ended_reason | csv | Filter on the conversation dimension — why a connected call ended. Exactly nine values: user_hangup, agent_hangup, silence_timeout, max_duration, voicemail, transferred, pipeline_error, cancelled, unknown. (See endedReason values.) |
agent_id | UUID | The agent's UUID (not the integer FK). |
agent_name | string | Substring match (case-insensitive) on the agent name captured at call creation. Transient agents are matched by the name field on the inline agent object. |
tag=rejected | tag | Returns only rejected attempts — see Rejected calls below. |
campaign_id | int | Campaign-dispatched calls only. |
phone_number | string | Matches from_number OR to_number (E.164). |
telephony_provider | string | twilio, vobiz, plivo, etc. |
from_date | ISO 8601 | Calls with created_at >= from_date. |
to_date | ISO 8601 | Calls with created_at <= to_date. |
tag | csv | Any of the given tags (logical OR). |
call_source | csv | api_outbound,api_inbound,dashboard_outbound,dashboard_inbound,webrtc,websocket,unknown. |
csv means comma-separated
For csv params, send the values comma-separated with no whitespace: ?status=completed,error.
Response
Returns HTTP 200 OK with the standard paginated envelope.
{
"items": [
{
"callId": "call_4e4e571f8c9b3cf8e99d",
"transport": "outbound",
"pipelineSource": "agent",
"status": "completed",
"connectionStatus": "completed",
"endedReason": "user_hangup",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"agentName": "Sales bot",
"campaignId": null,
"platformNumber": "+14155550100",
"customerNumber": "+14155559999",
"telephonyProvider": "twilio",
"callSource": "api_outbound",
"startedAt": "2026-06-16T10:30:12Z",
"endedAt": "2026-06-16T10:32:01Z",
"durationSeconds": 109,
"createdAt": "2026-06-16T10:30:10Z",
"cost": 0.0185,
"tags": ["api", "outbound"]
}
],
"total": 1234,
"page": 1,
"perPage": 50,
"totalPages": 25
}CallListItem fields
| Field | Type | Notes |
|---|---|---|
callId | string | zoxaAI's id. Use for GET /calls/{callId}. |
transport | string | One of outbound, inbound, webrtc, websocket, web. |
pipelineSource | string | agent for agent calls. |
status | string | pending, active, completed, error. |
connectionStatus | string | null | Did the call connect? completed when the callee answered / the web client joined; otherwise the reason it never connected. null only while the outcome is still unknown (in-flight). See connectionStatus values. |
endedReason | string | null | Why a connected call ended. Set once a connected call is terminal; always null for calls that never connected — read connectionStatus for those. See endedReason values. |
agentId / agentName | string | null | The agent in use (if any). |
campaignId | int | null | If dispatched by a campaign. |
platformNumber | string | null | Your number on this call (from-side for outbound, to-side for inbound). |
customerNumber | string | null | The customer's number (inverse of above). |
telephonyProvider | string | null | Provider name for phone calls. |
callSource | string | api_outbound, api_inbound, dashboard_outbound, dashboard_inbound, webrtc, websocket, unknown. |
startedAt / endedAt | ISO 8601 | null | Wall-clock times of pipeline start/end. |
durationSeconds | float | null | Active-call duration. |
createdAt | ISO 8601 | When the row was written (precedes startedAt). |
cost | float | null | Total cost in USD (LLM + TTS + STT). Telephony cost is the customer's own provider bill. |
tags | string[] | Free-form tags. Auto-tags include api, outbound, inbound, webcall, not_connected, telephony_<status>. |
Examples
Last 24 hours of API-first outbound calls that never connected
FROM=$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)
curl "https://dashboard.zoxa.ai/api/v1/calls?transport=outbound&call_source=api_outbound&connection_status=busy,no_answer,connection_failed,dial_failed&from_date=$FROM" \
-H "X-API-Key: zsk_..."const from = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const params = new URLSearchParams({
transport: "outbound",
call_source: "api_outbound",
connection_status: "busy,no_answer,connection_failed,dial_failed",
from_date: from,
});
const res = await fetch(`https://dashboard.zoxa.ai/api/v1/calls?${params}`, {
headers: { "X-API-Key": "zsk_..." },
});
const { items, total } = await res.json();from datetime import datetime, timedelta, timezone
import httpx
frm = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
resp = httpx.get(
"https://dashboard.zoxa.ai/api/v1/calls",
headers={"X-API-Key": "zsk_..."},
params={
"transport": "outbound",
"call_source": "api_outbound",
"connection_status": "busy,no_answer,connection_failed,dial_failed",
"from_date": frm,
},
)
data = resp.json()
print(data["total"], "not-connected calls in last 24h")Connected calls that crashed mid-conversation
curl "https://dashboard.zoxa.ai/api/v1/calls?connection_status=completed&ended_reason=pipeline_error" \
-H "X-API-Key: zsk_..."Calls for one agent, paginated
curl "https://dashboard.zoxa.ai/api/v1/calls?agent_id=550e8400-...&per_page=100&page=2" \
-H "X-API-Key: zsk_..."Errors
| Status | detail | When |
|---|---|---|
400 | "No organization selected" | Auth missing org context. |
401 | auth errors | See Errors. |
422 | array | Invalid sort_by, sort_order, or page/per_page out of range. |
The two outcome dimensions
Every call carries two orthogonal outcome fields:
connectionStatus— did the call connect?completedwhen the callee answered (telephony) or the client joined (web); otherwise the reason it never connected — a telephony outcome, a platform failure, or an API rejection.endedReason— why did a connected call end? Only populated once the conversation actually started. Alwaysnullfor calls that never connected.
┌─ NOT CONNECTED → connectionStatus = why | endedReason = null
call attempt ────┤
└─ CONNECTED → connectionStatus = "completed" | endedReason = why it endedconnectionStatus 'completed' vs status 'completed'
The success value of connectionStatus is completed — the connection succeeded and the pipeline started. It is deliberately a separate field from the top-level status (pending/active/completed/error): a call can have connectionStatus: "completed" and status: "error" (connected, then the pipeline crashed → endedReason: "pipeline_error").
connectionStatus values
Telephony / transport outcomes
| Value | When |
|---|---|
completed | Connection succeeded — callee answered / web client joined; the pipeline started. endedReason says how it ended. |
no_answer | Rang, never picked up (missed call). |
busy | Destination was on another call. |
rejected | Callee or carrier actively declined the call. |
invalid_number | The number doesn't exist (e.g. Telnyx unallocated_number). |
unreachable | Congestion / destination unreachable (e.g. Cloudonix CONGESTION). |
cancelled | Caller or system hung up before the call was answered. |
connection_failed | Provider reported a generic failed / error on the dial. |
no_participant | Web / WebRTC only — the client never joined within the join timeout. |
telephony_<raw> | Passthrough for a provider status we don't recognize — the raw status, sanitized (lowercased, non-alphanumerics → _). Unknown provider outcomes are surfaced, never silently swallowed. |
Platform failures (never dialed / never reached the provider)
| Value | When |
|---|---|
dial_failed | Our dial request to the provider was rejected (the provider_rejected 400/502 error). logs.dial_error has the provider's message. |
initiation_failed | The campaign dispatcher failed to start the call. |
missing_credentials | The pipeline couldn't resolve telephony credentials. |
concurrency_limit_reached | Rejected by zoxaAI's concurrent-call gate. logs.validation_error carries the limit and active count frozen at rejection time. |
insufficient_balance | Rejected by the wallet gate. logs.validation_error carries the balances at rejection time. Other wallet-gate codes (wallet_rejected, org_not_found, user_not_found, no_org) pass through the same way. |
API rejections (request never became a call)
Each rejection class is its own connectionStatus value, so one filter isolates one failure mode. See Rejected calls for how the raw request and rejection payload are preserved on the row.
| Value | When |
|---|---|
schema_validation | Request body failed schema or cross-field validation (including call-mode rules like sending both agentId and agent). |
snapshot_validation | Request shape was valid but the resolved agent config failed snapshot validation. |
model_validation | The request named an LLM/TTS/STT model that isn't in the model catalog. |
agent_not_found | agentId didn't resolve to an agent in your organization. |
credentials_invalid | Telephony provider rejected the inline credentials. |
inbound_invalid | Inbound registration failed — usually because the phone number isn't owned by the provider account. |
provider_not_supported | The chosen provider doesn't support this operation. |
endedReason values
Why a connected call ended. Exactly nine values — null until the call connects, and forever null for calls that never did (their outcome lives in connectionStatus).
| Value | When |
|---|---|
user_hangup | The user hung up. |
agent_hangup | The agent ended the call — the endCall tool fired. |
silence_timeout | The user stayed silent through every idle-prompt retry (userIdleTimeoutS × userIdleMaxRetries). |
max_duration | The call hit maxCallDurationS. |
voicemail | Answering-machine detection fired — a machine answered (the call did connect). |
transferred | The call was handed off via the transferCall tool. |
pipeline_error | Unhandled exception inside the pipeline after the call connected. The row's status is error. |
cancelled | The pipeline task was cancelled mid-call (service shutdown, manual abort). |
unknown | Rare fallback — the call connected and ended, but no component attributed the ending. A non-trivial unknown count is a bug signal, never a success label. |
Analytics buckets
Group on connectionStatus first, then split connected calls by endedReason:
- Success (conversation happened):
connectionStatus=completedwithendedReasoninuser_hangup,agent_hangup,silence_timeout,max_duration,voicemail,transferred,cancelled. - Connected but crashed:
connectionStatus=completed+endedReason=pipeline_error— the only connected ending that marks the rowstatus=error. - Connected, unattributed:
connectionStatus=completed+endedReason=unknown— count it separately; a rising number means something isn't reporting its exit. - Didn't connect (operational):
connectionStatusinno_answer,busy,rejected,invalid_number,unreachable,cancelled,no_participant,telephony_<raw>— normal telephony attrition, tune your dialing instead of debugging. - Didn't connect (platform failure):
connection_failed,dial_failed,initiation_failed,missing_credentials— investigate. - Rejected before start: the API-rejection values plus
concurrency_limit_reached/insufficient_balance— fix the request or the account, not the call.
Rejected calls
Every authenticated request that's rejected before the pipeline starts — bad schema, unresolvable agent, invalid credentials, tool errors, and gate rejections (concurrency limit, insufficient wallet balance) — creates a row in this list. The row has status="error", the rejected tag, the rejection reason in connectionStatus (from the API rejections table above), and endedReason: null — the call never connected, so there is no conversation ending to report. The configSnapshot on the detail response is the redacted request body the client sent (auth scrubbed), and logs.validation_error carries the full rejection payload (Pydantic error tree, or the concurrency/wallet state frozen at rejection time).
Gate rejections (concurrency_limit_reached, insufficient_balance) are throttled to one row per org+reason per minute so a client retry loop can't flood the table — the first row of a burst carries everything needed.
Dial-leg failures are different: they produce a real call row (connectionStatus: dial_failed, callSource: api_outbound) whose logs.dial_error carries the provider's error message and providerStatus. The callId in the provider_rejected error response links straight to it.
This makes it possible to diagnose API integration issues without bouncing screenshots between two teams: pull the call by id, see exactly what arrived, and see exactly why we rejected it.
# All rejected attempts in the last day for one org
curl "https://dashboard.zoxa.ai/api/v1/calls?tag=rejected&from_date=$(date -u -v-1d +%FT%TZ)" \
-H "X-API-Key: zsk_..."Filter further with connection_status=schema_validation (or any of the rejection values above) to isolate one class.
Related
GET /calls/{callId}— full detail incl. transcript, recordings, latency stats- Campaign runs — per-call run list scoped to a campaign
WebSocket audio protocol
WS /api/v1/ws/audio/{callId} — bidirectional raw-PCM audio + JSON events. Auth via api_key query string; close codes for invalid call states.
Get a call
GET /api/v1/calls/{callId} — full lifecycle for one call. Includes request snapshot, transcript, recordings, cost, per-turn latency, and raw events.