Calls Overview
How POST /api/v1/call dispatches between outbound phone, inbound phone, WebSocket, and WebRTC modes — plus the full list of call-related endpoints.
One endpoint, four modes
POST /api/v1/call is the single entry point for placing a call. The body discriminator (type for phone calls, transport for web calls) selects what gets created. Everything else — agent selection, context variables, webhook URL — works identically across modes.
The dispatch matrix
| Body has | Mode | What happens |
|---|---|---|
type: "outbound" + callConfig + toNumber | Phone — outbound | Dial out immediately via Twilio / Vobiz using inline credentials. Returns callId. |
type: "inbound" + callConfig | Phone — inbound | Register the number on the provider so future inbound calls reach your agent. Returns bindingId. Re-POSTing the same number is last-write-wins. |
transport: "websocket" (or omitted) | Web — raw audio | Create a call row, then connect wss://dashboard.zoxa.ai/api/v1/ws/audio/{callId} for raw-PCM streaming. |
transport: "webrtc" | Web — WebRTC | Create a pending call row over the built-in SmallWebRTC transport. Returns offerUrl; the browser POSTs its SDP offer there to start the pipeline. |
The four modes share the same agent resolution, context variables, and webhook URL handling. The provider plumbing differs only at the transport layer.
Endpoints in this section
| Method | Path | Purpose |
|---|---|---|
POST | /call | Place outbound phone call (type=outbound) |
POST | /call | Register inbound number (type=inbound) |
POST | /call | Create WebSocket call (transport=websocket) |
POST | /call | Create WebRTC call (transport=webrtc) |
WS | /ws/audio/{callId} | Bidirectional raw-PCM audio stream |
GET | /calls | List call history with filters |
GET | /calls/{callId} | Full call detail incl. transcript, recordings, cost, turns, events |
How agents are selected
Every mode accepts agent selection in one of two ways. The same rules apply to all four modes.
| Body fields | Mode | Use when |
|---|---|---|
agentId | Persistent | Use a saved agent unchanged. |
agent (inline) | Transient | One-off call with no saved record. The inline object is a full Agent Config — at minimum name and llm: { provider, model }. |
You cannot pass both agentId and agent — exactly one is required. For "saved agent but slightly different for this call," send the full modified config as a transient agent (fetch it with GET /agents/{uuid}, tweak, and inline it).
Transient agent — naming the run
The inline agent's required name is the label that surfaces in call history — useful so a run dispatched with a one-off inline config isn't anonymous in the dashboard.
{
"agent": {
"name": "Lead-qual bot — A/B variant 2",
"systemPrompt": "You qualify leads for a B2B SaaS.",
"llm": { "provider": "openai", "model": "gpt-5.4-mini" }
}
}The same name is what you filter against in GET /calls?agent_name=. Persistent calls automatically carry the saved agent's name, so the filter works uniformly across both modes.
Context variables
contextVariables is an optional {key: string} map. Values are substituted into {{key}} placeholders inside the resolved agent config (e.g., systemPrompt, greeting.firstMessages) before the call starts.
{
"agentId": "...",
"contextVariables": {
"customerName": "Aman",
"orderId": "ORD-42"
}
}If the agent's systemPrompt contains Hi {{customerName}}, your order {{orderId}}..., those values are filled before the LLM sees it.
Precedence
The values you send here are one layer of several. Highest priority wins:
| Priority | Source | Rule |
|---|---|---|
| 1 | Nothing declares the name | The literal {{token}} is left untouched |
| 2 | The agent's saved config.contextVariables | The default, used when this request omits the key. A saved blank renders as empty text |
| 3 | This request's contextVariables | Overrides the saved default — but only when the value is non-blank |
| 4 | System variables | Always win, see below |
Two consequences worth planning for:
- Sending
""does not blank a variable. An empty or whitespace-only value is treated as "not provided" and falls back to the agent's saved default. To render nothing, save a blank default on the agent instead. - System variables cannot be overridden.
current_time,current_day,current_date,current_timezone,user_numberandagent_numberare resolved by the platform and are silently ignored if you send them here. See Variables.
Keys match case- and space-insensitively, so customerName, customername and customer name all fill {{customerName}}.
Lifecycle webhook
The lifecycle webhook lives inside the agent config — webhook: { "url": "https://…", "headers": { … } } on the saved agent or the transient inline agent. Payload shape and event types are documented under Webhook Events.
What's persisted
Every call — phone or web — creates one row in the calls table. After the call ends you can retrieve:
requestSnapshot— the literal POST body (auth scrubbed) so you can see exactly what triggered the calltranscript— full conversation transcriptrecordings— array of signed audio URLsconnectionStatus— did the call connect:completed, or the reason it never did (busy,no_answer,rejected,dial_failed,no_participant, …). SeeconnectionStatusvalues.endedReason— why a connected call ended:user_hangup,agent_hangup,silence_timeout,max_duration,voicemail,transferred,pipeline_error,cancelled,unknown.nullwhen the call never connected. SeeendedReasonvalues.durationSeconds,cost,costBreakdown,usageInfoturns— per-turn latency breakdown (LLM TTFB, TTS TTFB, STT TTFB, tool-call totals)events— fine-grained pipeline events (turn starts/ends, interruptions, errors)analysis— post-call summary if analysis is enabledlatencySummary— aggregated TTFB stats
See GET /calls/{callId} for the full response shape.
Reacting to call lifecycle: webhooks vs polling
| Webhooks | Polling | |
|---|---|---|
| How | zoxaAI POSTs call.started / call.ended to agent.webhook.url | You GET /calls/{callId} on an interval until terminal |
| Latency | Push — sub-second after the event | interval seconds, can be slow |
| Reliability | At-least-once with retry on 5xx | At-most-once-per-poll, you bear the timeout |
| Setup cost | Need a public endpoint with webhook_secret verification | Just an API key |
| Best for | Production systems with their own queue/handler | Scripts, debugging, batch jobs without a public endpoint |
You can use both: set agent.webhook and also poll as a backup. Make your webhook handler idempotent on (call_id, event) — events may be redelivered.
Quick decision tree
Are you connecting a phone number?
├── Yes, calling someone now → POST /call type=outbound
└── Yes, receiving calls on a number → POST /call type=inbound
then phone the number — inbound webhook
lands on the registered binding
Are you in a browser or your own audio pipeline?
├── Browser with WebRTC → POST /call transport=webrtc, then POST the
SDP offer to the returned offerUrl
└── Your own server with raw PCM → POST /call (transport=websocket is the default)
then WS to /ws/audio/{callId}