zoxaAI
Homepage
API ReferenceCalls

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 hasModeWhat happens
type: "outbound" + callConfig + toNumberPhone — outboundDial out immediately via Twilio / Vobiz using inline credentials. Returns callId.
type: "inbound" + callConfigPhone — inboundRegister 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 audioCreate a call row, then connect wss://dashboard.zoxa.ai/api/v1/ws/audio/{callId} for raw-PCM streaming.
transport: "webrtc"Web — WebRTCCreate 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

MethodPathPurpose
POST/callPlace outbound phone call (type=outbound)
POST/callRegister inbound number (type=inbound)
POST/callCreate WebSocket call (transport=websocket)
POST/callCreate WebRTC call (transport=webrtc)
WS/ws/audio/{callId}Bidirectional raw-PCM audio stream
GET/callsList 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 fieldsModeUse when
agentIdPersistentUse a saved agent unchanged.
agent (inline)TransientOne-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:

PrioritySourceRule
1Nothing declares the nameThe literal {{token}} is left untouched
2The agent's saved config.contextVariablesThe default, used when this request omits the key. A saved blank renders as empty text
3This request's contextVariablesOverrides the saved default — but only when the value is non-blank
4System variablesAlways 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_number and agent_number are 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 call
  • transcript — full conversation transcript
  • recordings — array of signed audio URLs
  • connectionStatus — did the call connect: completed, or the reason it never did (busy, no_answer, rejected, dial_failed, no_participant, …). See connectionStatus values.
  • endedReason — why a connected call ended: user_hangup, agent_hangup, silence_timeout, max_duration, voicemail, transferred, pipeline_error, cancelled, unknown. null when the call never connected. See endedReason values.
  • durationSeconds, cost, costBreakdown, usageInfo
  • turns — 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 enabled
  • latencySummary — aggregated TTFB stats

See GET /calls/{callId} for the full response shape.

Reacting to call lifecycle: webhooks vs polling

WebhooksPolling
HowzoxaAI POSTs call.started / call.ended to agent.webhook.urlYou GET /calls/{callId} on an interval until terminal
LatencyPush — sub-second after the eventinterval seconds, can be slow
ReliabilityAt-least-once with retry on 5xxAt-most-once-per-poll, you bear the timeout
Setup costNeed a public endpoint with webhook_secret verificationJust an API key
Best forProduction systems with their own queue/handlerScripts, 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}

On this page