Widget public API

These endpoints are what the widget script uses. You only need them to build your own widget UI; the standard script handles all of this for you. They need no API key. Instead, each request must come from one of the widget's allowed origins, and calls that control a session must present that session's private endToken.

Base URL: https://api.in.zlicx.ai/api/v1. Every response is wrapped as { success, message, data }; errors use { success: false, code, message }.

CORS and origins

zlicx answers browser requests only from the widget's allowed origins, including on errors, so your page can read the message. A request from any other origin gets 403. Allowed methods are GET, POST, and OPTIONS; the only allowed request header is Content-Type.

GET /public/widgets/:widgetId/config

The widget's public settings. Returns 404 if the widget is disabled or its agent is not ready.

json
{
  "widgetId": "wgt_...",
  "name": "Support line",
  "agentName": "Asha",
  "theme": { "primaryColor": "#902986", "position": "bottom-right", "...": "..." },
  "consentRequired": true,
  "consentText": "This voice call may be recorded and transcribed.",
  "sessionTtlSeconds": 900
}

POST /public/widgets/:widgetId/sessions

Starts a call. Request the microphone before calling this, so a refused permission does not use up the visitor's session allowance.

json
{ "visitorId": "optional anonymous ID, up to 120 characters" }

The response tells you how to connect, in the transport field.

transport: "engine"

Connect with plain WebRTC through the offer endpoint below.

json
{
  "transport": "engine",
  "sessionId": "wgs_...",
  "endToken": "keep this private",
  "roomName": "widget_wgs_...",
  "expiresAt": "2026-11-03T10:15:00.000Z",
  "iceServers": [
    { "urls": ["stun:stun.l.google.com:19302"] },
    { "urls": ["turn:turn.example.com:3478?transport=udp"], "username": "1793...:a1b2", "credential": "..." }
  ]
}

transport: "livekit"

Join the LiveKit room with livekitUrl and participant.token. Older responses without a transport field are LiveKit sessions.

json
{
  "transport": "livekit",
  "sessionId": "wgs_...",
  "endToken": "keep this private",
  "livekitUrl": "wss://...",
  "roomName": "widget_wgs_...",
  "participant": { "identity": "widget-user-...", "name": "Website visitor", "token": "...", "ttlSeconds": 900 },
  "expiresAt": "2026-11-03T10:15:00.000Z"
}

Errors:

  • 404: the widget is disabled or not found.
  • 402: the account has no credit.
  • 429: too many sessions from this visitor, or the widget is at its concurrent-call limit.
  • 503: the voice service is unavailable; try again.

POST /public/widgets/:widgetId/sessions/:sessionId/offer

Engine sessions only. Send the browser's WebRTC offer and get the engine's answer. The engine does not use trickle ICE: wait until ICE gathering is complete (or about 3 seconds) so every candidate is in the offer. Each session accepts one offer.

json
{ "endToken": "...", "sdp": "v=0\r\no=- ..." }
json
{
  "legId": "2c973971-...",
  "sdp": "v=0\r\no=- ...",
  "candidates": [{ "candidate": "candidate:1 1 udp ...", "sdpMid": "0", "sdpMLineIndex": 0 }]
}

Apply the answer with setRemoteDescription, then add each candidate. The agent starts speaking as soon as the media connection is up.

javascript
const pc = new RTCPeerConnection({ iceServers: session.iceServers });
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
mic.getAudioTracks().forEach((track) => pc.addTrack(track, mic));
pc.ontrack = (event) => { audio.srcObject = event.streams[0]; };

await pc.setLocalDescription(await pc.createOffer());
await waitForIceGatheringComplete(pc); // up to ~3 seconds

const res = await fetch(`${api}/public/widgets/${widgetId}/sessions/${session.sessionId}/offer`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ endToken: session.endToken, sdp: pc.localDescription.sdp }),
});
const { data } = await res.json();
await pc.setRemoteDescription({ type: "answer", sdp: data.sdp });
for (const candidate of data.candidates) await pc.addIceCandidate(candidate);

Errors:

  • 404: wrong endToken, or the session ended or expired.
  • 400: the session is already connected, or the SDP is not an offer.
  • 403: the request came from an origin that is not allowed.

POST /public/widgets/:widgetId/sessions/:sessionId/end

Hangs up the call. Send it when the visitor ends the call or leaves the page. Ending an already-ended session is harmless.

json
{ "endToken": "..." }
json
{ "status": "ended", "roomName": "widget_wgs_..." }

The call log is written shortly after the call ends, once the transcript is complete and any recording has finished uploading.

Related