zoxaAI
Homepage
API ReferenceCalls

List calls

GET /api/v1/calls — paginated, filterable list of every call placed in your organization.

GET /api/v1/calls

Paginated 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

ParamTypeDefaultNotes
pageint (≥1)11-indexed page number.
per_pageint (1..100)50Items per page; capped at 100.
sort_byenumended_atOne of ended_at, created_at, duration_seconds, cost.
sort_orderasc / descdesc

ended_at sort puts NULLs last so in-flight calls don't crowd the top of the default view.

Filters

ParamTypeDescription
call_idstringExact match on callId.
transportcsvOne 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.
statuscsvpending,active,completed,error — any combination.
connection_statuscsvFilter 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_reasoncsvFilter 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_idUUIDThe agent's UUID (not the integer FK).
agent_namestringSubstring 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=rejectedtagReturns only rejected attempts — see Rejected calls below.
campaign_idintCampaign-dispatched calls only.
phone_numberstringMatches from_number OR to_number (E.164).
telephony_providerstringtwilio, vobiz, plivo, etc.
from_dateISO 8601Calls with created_at >= from_date.
to_dateISO 8601Calls with created_at <= to_date.
tagcsvAny of the given tags (logical OR).
call_sourcecsvapi_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

FieldTypeNotes
callIdstringzoxaAI's id. Use for GET /calls/{callId}.
transportstringOne of outbound, inbound, webrtc, websocket, web.
pipelineSourcestringagent for agent calls.
statusstringpending, active, completed, error.
connectionStatusstring | nullDid 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.
endedReasonstring | nullWhy 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 / agentNamestring | nullThe agent in use (if any).
campaignIdint | nullIf dispatched by a campaign.
platformNumberstring | nullYour number on this call (from-side for outbound, to-side for inbound).
customerNumberstring | nullThe customer's number (inverse of above).
telephonyProviderstring | nullProvider name for phone calls.
callSourcestringapi_outbound, api_inbound, dashboard_outbound, dashboard_inbound, webrtc, websocket, unknown.
startedAt / endedAtISO 8601 | nullWall-clock times of pipeline start/end.
durationSecondsfloat | nullActive-call duration.
createdAtISO 8601When the row was written (precedes startedAt).
costfloat | nullTotal cost in USD (LLM + TTS + STT). Telephony cost is the customer's own provider bill.
tagsstring[]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

StatusdetailWhen
400"No organization selected"Auth missing org context.
401auth errorsSee Errors.
422arrayInvalid 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? completed when 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. Always null for calls that never connected.
                 ┌─ NOT CONNECTED → connectionStatus = why   |  endedReason = null
call attempt ────┤
                 └─ CONNECTED     → connectionStatus = "completed"  |  endedReason = why it ended

connectionStatus '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

ValueWhen
completedConnection succeeded — callee answered / web client joined; the pipeline started. endedReason says how it ended.
no_answerRang, never picked up (missed call).
busyDestination was on another call.
rejectedCallee or carrier actively declined the call.
invalid_numberThe number doesn't exist (e.g. Telnyx unallocated_number).
unreachableCongestion / destination unreachable (e.g. Cloudonix CONGESTION).
cancelledCaller or system hung up before the call was answered.
connection_failedProvider reported a generic failed / error on the dial.
no_participantWeb / 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)

ValueWhen
dial_failedOur dial request to the provider was rejected (the provider_rejected 400/502 error). logs.dial_error has the provider's message.
initiation_failedThe campaign dispatcher failed to start the call.
missing_credentialsThe pipeline couldn't resolve telephony credentials.
concurrency_limit_reachedRejected by zoxaAI's concurrent-call gate. logs.validation_error carries the limit and active count frozen at rejection time.
insufficient_balanceRejected 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.

ValueWhen
schema_validationRequest body failed schema or cross-field validation (including call-mode rules like sending both agentId and agent).
snapshot_validationRequest shape was valid but the resolved agent config failed snapshot validation.
model_validationThe request named an LLM/TTS/STT model that isn't in the model catalog.
agent_not_foundagentId didn't resolve to an agent in your organization.
credentials_invalidTelephony provider rejected the inline credentials.
inbound_invalidInbound registration failed — usually because the phone number isn't owned by the provider account.
provider_not_supportedThe 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).

ValueWhen
user_hangupThe user hung up.
agent_hangupThe agent ended the call — the endCall tool fired.
silence_timeoutThe user stayed silent through every idle-prompt retry (userIdleTimeoutS × userIdleMaxRetries).
max_durationThe call hit maxCallDurationS.
voicemailAnswering-machine detection fired — a machine answered (the call did connect).
transferredThe call was handed off via the transferCall tool.
pipeline_errorUnhandled exception inside the pipeline after the call connected. The row's status is error.
cancelledThe pipeline task was cancelled mid-call (service shutdown, manual abort).
unknownRare 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=completed with endedReason in user_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 row status=error.
  • Connected, unattributed: connectionStatus=completed + endedReason=unknown — count it separately; a rising number means something isn't reporting its exit.
  • Didn't connect (operational): connectionStatus in no_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.

On this page