zoxaAI
Homepage
API ReferenceCalls

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/call

Create 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.

FieldTypeRequiredDefaultDescription
transport"webrtc"✓"websocket"Must be sent explicitly — default is websocket.
agentIdstringone of two—A saved agent's UUID. String validation only.
agentobjectone 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"
}
FieldTypeDescription
callIdstringzoxaAI's internal id. Use for history lookups.
offerUrlstringThe 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:

MethodPathPurpose
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 agent

What zoxaAI does after the 201

  1. Resolves the agent config (persistent or transient), substitutes contextVariables, writes the snapshot to the calls row as pending.
  2. Validates inline tools (same rules as the WebSocket transport).
  3. Returns the offerUrl. No pipeline runs yet.
  4. When the browser POSTs its SDP offer to offerUrl, the server answers via pipecat's SmallWebRTCRequestHandler and 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:

StatusWhen
404call_id doesn't resolve to a pending webrtc call in your org.
409A 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.

On this page