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.
{
"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.
{ "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.
{
"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.
{
"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.
{ "endToken": "...", "sdp": "v=0\r\no=- ..." }{
"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.
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: wrongendToken, 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.
{ "endToken": "..." }{ "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
- Website widget guide
- API reference for managing widgets with an API key.

