Start a widget session
Opens (or resumes) a visitor session and returns what the chat UI needs to render: greeting, branding
and whether voice is on. Call it once when the chat loads, then use the returned session_id with
POST /widget/message, GET /widget/poll and GET /widget/history. The standard embed script does all
of this for you.
Credentials. This is a public endpoint. It does not accept a dashboard session or a jl_ API key.
Name the widget with exactly one of these, checked in this order:
x-widget-api-keyheader: the secretplaintext_api_key(server-side callers only);x-widget-idheader: the public widget id (what the embed uses);widquery parameter: the public widget id.
The API key is never accepted in the query string. A value that looks like a UUID is treated as a
widget id, whichever header carries it. Every call must also send x-widget-parent-origin with the
origin of the page that embeds the widget; it must match one of the widget’s allowed_origins
(scheme and port exactly, hostname modulo one leading www.). The check does not use the browser’s
Origin header, and CORS is open (Access-Control-Allow-Origin reflects any origin, no credentials).
Sessions. Sessions are keyed by visitor_id. If the same visitor had a session on this widget
that was active in the last 30 days, it is returned with resumed: true and any new metadata is
merged into it (keys are added or overwritten, never removed); otherwise a new session is created.
Send metadata.email (and metadata.name) to link the visitor’s conversation to a contact.
Side effects. Creates or updates a widget_sessions row (with the caller’s IP and user agent on
creation). No conversation is created until the first message; no AI usage is recorded.
Idempotency. Safe to retry with the same visitor_id: a retry resumes the session the first call
created. Two truly concurrent first calls for a new visitor can create two sessions.
Access
- Required scope: None;
jl_API keys are not used. The widget credential plus a matchingx-widget-parent-originauthorise the call. - Rate limit: Widget public — 60 requests/min per IP, plus the widget’s
rate_limit_rpm(default 30) per credential and 300 requests/min per workspace across all its widgets. See Rate limits. - Plan: Available on every plan.
Headers
Secret widget API key (plaintext_api_key from POST /api/widgets). Takes precedence over
x-widget-id and wid. Send one of the three. Never expose it in a browser.
Public widget id, the credential the embed script uses. Used when x-widget-api-key is absent.
Origin (or any URL; only its origin is used) of the page embedding the widget. Must match one of the
widget’s allowed_origins. Missing returns 403; an unparseable value returns 403 Invalid parent origin.
Only the dashboard preview, which sends a short-lived x-widget-preview-token, may omit it.
Query parameters
Request
Stable id the embedding page keeps for this visitor (the embed stores a UUID in
localStorage). Reusing it within 30 days resumes the same session and conversation.
Free-form visitor data stored on the session (merged on resume). email links the
conversation to a contact (created in the workspace’s manual-conversations campaign if none
matches, case-insensitively); name fills the contact’s name when it is empty.
Response
Session created or resumed (both return 201).

