Create a WebRTC call
POST /api/v1/call with transport=webrtc — create a browser call over SmallWebRTC. Complete the SDP offer/answer handshake at the returned offerUrl.
POST /api/v1/callCreate a browser WebRTC call over zoxaAI's built-in SmallWebRTC transport. Use this when the audio endpoint is a browser — SmallWebRTC handles the peer connection, ICE, and renegotiation so you don't have to run your own media server.
When to use which transport
WebRTC (this page) → browsers, end-user apps, anywhere you'd otherwise need ICE/STUN/TURN. WebSocket → server-side audio pipelines where you control the codec end-to-end.
Authentication
X-API-Key: zsk_...Request body
type is omitted for the WebRTC path.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
transport | "webrtc" | ✓ | "websocket" | Must be sent explicitly — default is websocket. |
agentId | string | one of two | — | A saved agent's UUID. String validation only. |
agent | object | one of two | — | Inline transient agent — a full Agent Config; at minimum name + llm: { provider, model }. Provide exactly one of agentId / agent. |
contextVariables | {string: string} | — | — | Substituted into the agent config at call start. |
Lifecycle webhook lives inside the agent config's webhook field (saved or inline). See Webhooks for the shape.
Response
Returns HTTP 201 Created.
{
"callId": "call_4e4e571f8c9b3cf8e99d",
"offerUrl": "/api/v1/webrtc/offer/call_4e4e571f8c9b3cf8e99d",
"transport": "webrtc",
"status": "pending"
}| Field | Type | Description |
|---|---|---|
callId | string | zoxaAI's internal id. Use for history lookups. |
offerUrl | string | The SmallWebRTC signaling endpoint. The browser POSTs its SDP offer here to establish the media session — see SmallWebRTC signaling below. |
transport | "webrtc" | Echo of the request. In call history this row is stored with transport="web" — that's the value you filter on at GET /calls?transport=web. |
status | "pending" | The call row is created pending; the pipeline launches (and the row goes active) when the SDP offer arrives at offerUrl. |
SmallWebRTC signaling
The 201 does not start the pipeline. The browser completes a standard WebRTC handshake against offerUrl (/api/v1/webrtc/offer/{call_id}), authenticated with the same X-API-Key:
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/webrtc/offer/{call_id} | Send the browser's SDP offer; the server answers and — on the first offer — launches the agent pipeline. Renegotiations (an offer carrying an existing pc_id) reuse the live peer connection. |
PATCH | /api/v1/webrtc/offer/{call_id} | Add trickle-ICE candidates to an in-flight peer connection. |
The offer handshake happens exactly once per call — a duplicate offer on a call whose pipeline is already starting returns 409.
Examples
Create the call
curl -X POST https://dashboard.zoxa.ai/api/v1/call \
-H "X-API-Key: zsk_..." \
-H "Content-Type: application/json" \
-d '{
"transport": "webrtc",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"contextVariables": { "customerName": "Aman" }
}'const res = await fetch("https://dashboard.zoxa.ai/api/v1/call", {
method: "POST",
headers: {
"X-API-Key": "zsk_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
transport: "webrtc",
agentId: "550e8400-e29b-41d4-a716-446655440000",
contextVariables: { customerName: "Aman" },
}),
});
const { callId, offerUrl } = await res.json();
// Next: POST the browser's SDP offer to offerUrl (see below).import httpx
resp = httpx.post(
"https://dashboard.zoxa.ai/api/v1/call",
headers={"X-API-Key": "zsk_..."},
json={
"transport": "webrtc",
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"contextVariables": {"customerName": "Aman"},
},
)
data = resp.json()
call_id, offer_url = data["callId"], data["offerUrl"]Complete the handshake from the browser
const pc = new RTCPeerConnection();
// ...add the user's mic track, wire up ontrack for the agent's audio...
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// POST the SDP offer to the returned offerUrl; the server answers and starts the agent.
const answer = await fetch(`https://dashboard.zoxa.ai${offerUrl}`, {
method: "POST",
headers: { "X-API-Key": "zsk_...", "Content-Type": "application/json" },
body: JSON.stringify({ sdp: pc.localDescription.sdp, type: pc.localDescription.type }),
}).then((r) => r.json());
await pc.setRemoteDescription(answer);
// Trickle ICE candidates as they're gathered:
pc.onicecandidate = ({ candidate }) => {
if (!candidate) return;
fetch(`https://dashboard.zoxa.ai${offerUrl}`, {
method: "PATCH",
headers: { "X-API-Key": "zsk_...", "Content-Type": "application/json" },
body: JSON.stringify({ candidate }),
});
};
// audio I/O is now flowing between the user's browser and the agentWhat zoxaAI does after the 201
- Resolves the agent config (persistent or transient), substitutes
contextVariables, writes the snapshot to the calls row aspending. - Validates inline tools (same rules as the WebSocket transport).
- Returns the
offerUrl. No pipeline runs yet. - When the browser POSTs its SDP offer to
offerUrl, the server answers via pipecat'sSmallWebRTCRequestHandlerand launches the agent pipeline. The pipeline task is tracked in a strong-ref set so it doesn't get garbage-collected mid-call.
The agent speaks first (if greeting.speakFirst="agent") the moment the peer connection is established.
Errors
Same set as the WebSocket mode. The offer/PATCH signaling endpoints can additionally return:
| Status | When |
|---|---|
404 | call_id doesn't resolve to a pending webrtc call in your org. |
409 | A duplicate offer arrived for a call whose pipeline is already starting — the offer handshake happens exactly once per call. |
Call duration limit
The pipeline enforces the config's maxCallDurationS (default 610 seconds) — the call ends when the limit is reached. Raise maxCallDurationS in the agent config to extend.
Related
- WebSocket mode — server-side alternative
GET /calls/{callId}— transcript, recordings, latency after the call- Agent config — including
maxCallDurationS
Create a WebSocket call
POST /api/v1/call (no type) with transport=websocket — create a call backed by a raw-PCM WebSocket. Two-step flow — create the call, then connect /ws/audio/{callId}.
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.