Skip to content

REST endpoints

Base URL: https://leflux.xrlabs.app/api (or your self-hosted equivalent).

All endpoints accept + return JSON. CORS is open by default; restrict in nginx for production self-hosts.

POST /api/session/init

Initialize a new visitor session. Called by embed.js on widget mount.

Request

{
"siteHost": "acme.com",
"pageUrl": "https://acme.com/pricing"
}

Headers:

  • Origin: https://acme.com — the only auth signal. The server matches this against every site’s allowed-host list. There is no API token, no header secret, no Authorization header for the public widget API. Browsers prevent JS from forging cross-origin Origin, which makes it a sufficient signal for site identity.

Response 200

{
"sessionId": "uuid",
"wsUrl": "wss://leflux.xrlabs.app",
"siteConfig": {
"layout": "floating",
"primaryColor": "#a855f7",
"greeting": "Hi! How can I help?",
"position": "bottom-right",
"typography": { "headingFont": "...", "bodyFont": "...", "buttonRadius": "8px" },
"launcher": { "shape": "circle", "size": "md", "icon": "chat", "glow": true },
"nudge": { "enabled": false },
"quickChips": [{ "text": "Pricing", "payload": "Show pricing" }]
}
}

Response 403 — host not registered

{
"error": "host_not_registered",
"message": "Host \"x\" is not registered with LeFlux...",
"host": "x",
"signupUrl": "https://leflux.xrlabs.app/signup"
}

Rate limit: 10 requests per minute per IP.

POST /api/chat

Send a visitor message. Asynchronous — response is {messageId, status:"processing"} and the actual reply streams over the WebSocket.

Request

{
"sessionId": "uuid",
"message": "show me pricing",
"pageContext": {
"url": "https://acme.com/pricing",
"title": "Pricing — Acme",
"indexedElements": [
{ "id": 1, "type": "button", "text": "Get started", "parent": "Hero" }
],
"visibleText": "Choose a plan...",
"forms": [],
"previouslyFilledFields": []
}
}

Limits: message ≤ 5000 chars. indexedElements truncated server-side to 50.

Response 200

{
"messageId": "msg-1779723850877",
"status": "processing"
}

The actual LLM reply + actions arrive on the WebSocket as message_chunk / message_done / action_plan events.

Rate limit: 30 requests per minute per session.

GET /api/session/:id

Restore an existing session. Used on widget re-mount (page reload, tab reopen).

Response 200

{
"sessionId": "uuid",
"exists": true,
"host": "acme.com",
"messageCount": 7,
"siteConfig": {... same as session/init},
"history": [
{ "role": "user", "content": "show pricing" },
{ "role": "assistant", "content": "Here are the plans..." }
]
}

Response 404 when the session was server-evicted (past 30min idle).

DELETE /api/session/:id

Permanently delete a session. Used by visitors who want to clear their chat.

Response 200{ deleted: true }.

GET /api/stats

Server health + session stats. Public.

Response

{
"uptimeSeconds": 12345,
"activeSessions": 47,
"messagesProcessed": 89234,
"version": "1.0.0"
}

GET /health

Bare health check. Returns { status: "ok", timestamp: "..." }. Used by nginx upstream health + GitHub Actions deploy verification.

Errors

All errors follow a single shape:

{
"error": "machine_readable_code",
"message": "human readable explanation",
"details": { ... optional context ... }
}

Common codes:

CodeHTTPWhen
host_not_registered403Visitor’s host not in allowed-host list.
session_not_found404Session expired or never existed.
rate_limit_exceeded429Too many requests; back off.
invalid_message400Validation failure (message too long, missing fields).
internal_error500Unexpected server fault. Logged + investigated.

Auth

Public endpoints (session/init, chat, health, stats) authenticate exclusively via the request Origin header against the allowed-host list. No token, no API key, no Authorization header required.

Dashboard endpoints (/api/admin/*) require Firebase Auth ID token in Authorization: Bearer <jwt> header — those manage the tenant config and DO need real auth. See Self-hosting for full admin API details.