Developers
Integration guide
Losono agents can be integrated in three ways: a drop-in embed widget for chat and voice, a custom API for server-side chat and bespoke UIs, or lead tracking to capture external form submissions and click events on your existing site. All paths use the same published agent and can be combined — they differ in setup, auth, and what you build on your side.
Quick reference
- Embed loader
https://losono.xorora.com/embed.js- Hosted widget
https://losono.xorora.com/embed/{slug}- Chat API
POST https://losono.xorora.com/api/agents/{agentId}/chat- Voice API
GET/POST https://losono.xorora.com/api/agents/{agentId}/voice?mode=deploy- API key prefix
losono_sk_- Track loader
https://losono.xorora.com/track.js- Track API
POST https://losono.xorora.com/api/agents/{agentId}/track- Freeform form API
POST https://losono.xorora.com/api/agents/{agentId}/forms/submit- Registered form API
POST https://losono.xorora.com/api/agents/{agentId}/forms/{formId}/submit- Public slug lookup
GET https://losono.xorora.com/api/public/agents/{slug}- Track config API
GET https://losono.xorora.com/api/agents/{agentId}/track/config- Visitor ID key
losono_visitor_id(shared with embed widget)- Agent identifiers
- Embed and track.js use slug · API uses agentId (UUID from dashboard URL)
Choose an integration
Pick the path that matches your product. You can combine all three on the same agent.
| Embed widget | Custom API | Lead tracking | |
|---|---|---|---|
| Best for | Marketing sites, docs, Shopify, Webflow | Server-side chat, mobile apps, fully custom UI | Existing site forms and intent clicks (brochure downloads, product views) |
| Setup time | One script tag | API key + HTTP/WebSocket client | One script tag (+ optional server webhook) |
| API key | Not required | Required (or allowed-origin browser calls) | Not required in browser; required for server-side form POST |
| Chat | Built-in UI | You implement UI + streaming parser | N/A |
| Voice | Toggle on deploy page (Pro plan) | WebSocket to Gemini Live via session token (Pro plan) | N/A |
| CRM sync | Pre-chat form (auto) | N/A | External forms (auto); sessions (manual from dashboard) |
| Identifier | data-agent="slug" | agentId in URL path | data-agent="slug" in browser; agentId for server webhooks |
Embed widget (recommended)
Add a floating chat launcher to any website with a single script tag. Losono hosts the widget UI in a secure iframe — no API key is exposed on your domain.
When to use the embed
- You want the fastest path to production on a website.
- You are fine with Losono's default chat (and optional voice) UI.
- You do not want to manage API keys in your frontend.
- You need chat + optional voice without building WebSocket audio capture.
Prerequisites
- A Losono account with at least one agent configured (prompt + optional context files).
- The agent must be published.
- For voice in the embed: Pro subscription, voice enabled on the agent, and Chat + voice mode on the deploy page.
Step-by-step
1. Create and configure your agent
Sign in to the dashboard, create an agent, write a system prompt, and upload any context documents you want the agent to reference.
2. Test in the playground
Open the agent playground and verify chat (and voice, if needed) before going live. Playground mode uses your logged-in session — it is separate from the public embed.
3. Publish the agent
Go to Deploy and click Publish. The embed URL and script snippet are inactive until the agent is published.
4. Configure embed settings (optional)
On the deploy page, customize:
- Greeting — first message shown in the widget
- Primary color — header and user bubble color
- Launcher position — bottom-right (default) or bottom-left
- Modes — chat only, or chat + voice
- Allowed origins — restrict which domains may call the chat API from the iframe (empty = allow all)
Click Save embed settings after changes.
5. Copy the script snippet
From the deploy page, copy the script snippet. It looks like this:
<script src="https://losono.xorora.com/embed.js" data-agent="your-agent-slug"></script>
6. Add the script to your site
Paste the snippet before the closing
</body>tag on every page where you want the widget. The loader creates a fixed-position iframe pointing at Losono's hosted widget.Minimal HTML example
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>My site</title> </head> <body> <h1>Welcome</h1> <!-- Losono embed — one line --> <script src="https://losono.xorora.com/embed.js" data-agent="your-agent-slug"></script> </body> </html>7. Verify
Load your site. You should see a circular launcher in the corner. Click it to open chat. Messages stream in real time and conversations persist per browser via localStorage.
Script attributes
| Attribute | Required | Description |
|---|---|---|
data-agent | Yes | Published agent slug (from deploy page) |
data-position | No | bottom-right (default) or bottom-left |
src | Yes | Must point at https://losono.xorora.com/embed.js. The loader derives the iframe origin from this URL. |
Custom launcher position
<script src="https://losono.xorora.com/embed.js" data-agent="your-agent-slug" data-position="bottom-left"> </script>
Enabling voice in the embed
1. Upgrade to Pro and enable voice on the agent
Voice requires an active Pro subscription and voice enabled for that agent in the dashboard.
2. Set modes to Chat + voice
On the deploy page, change Modes to Chat + voice and save.
3. Redeploy the script (no changes needed)
The hosted iframe picks up settings server-side. Users will see Chat and Voice tabs inside the widget. Microphone permission is requested when starting a voice session.
The embed loader sets allow="microphone" on the iframe so voice works inside the hosted widget.
Alternative: direct iframe URL
Instead of the script loader, you can embed the full widget page directly. Useful for internal tools or when you control iframe sizing yourself.
<iframe src="https://losono.xorora.com/embed/your-agent-slug" title="Losono agent" allow="microphone" style="border:0;width:400px;height:640px;"> </iframe>
Hosted URL: https://losono.xorora.com/embed/your-agent-slug
How the embed works (for implementers and LLMs)
embed.jsreadsdata-agentand creates an iframe athttps://losono.xorora.com/embed/{slug}.- The iframe loads Losono's
EmbedWidgetcomponent, which callsPOST /api/agents/{agentId}/chatwithmode: "chat"and a generatedvisitorId. - Auth for chat requests from the iframe uses the
Refererheader (/embed/{slug}) — no API key on your domain. - The parent page and iframe communicate via
postMessage(losono:embed:resize,losono:embed:close) to resize the launcher and handle overlay clicks. - Conversations and messages are stored in the visitor's browser localStorage under keys prefixed with
losono_. - The embed widget and
track.jsshare the samelosono_visitor_idlocalStorage key. A visitor who submits an external form and later opens the chat widget can be correlated by visitor ID in the dashboard.
Custom API integration
Build your own chat UI or voice client. Use a server-side API key for backend integrations; use allowed origins only when you must call chat from a browser without exposing a secret.
When to use the custom API
- Chat runs on your server (e.g. support ticket enrichment, Slack bot).
- You need a fully branded UI that does not use the Losono widget.
- You are building a mobile or desktop app.
- You want programmatic control over message history and streaming.
Prerequisites
- Agent is published.
- You know the agent's agentId (UUID in the dashboard URL:
/agents/{agentId}/...). - An API key created on the deploy page (for server-side or voice).
- For voice: Pro plan, voice enabled on the agent, and a client that can capture/play PCM audio.
Step-by-step
1. Publish the agent
Same as the embed path — the agent must be published before deploy APIs accept traffic.
2. Copy the agent ID
Open your agent in the dashboard. The URL contains the UUID:
/agents/550e8400-e29b-41d4-a716-446655440000/deploy
Use this
agentIdin all API paths. Do not confuse it with the embedslug.3. Create an API key
On the deploy page, enter a key name (e.g. "Production backend") and click Generate key. Copy the secret immediately — it is shown only once.
losono_sk_...
Store keys in environment variables or a secrets manager. Never commit them to git or ship them in client-side bundles.
4. Send a chat message
POST to the chat endpoint with
mode: "chat"(required for deployed access). Include the full message history on each request.curl — streaming chat
curl -N -X POST "https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/chat" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer losono_sk_..." \ -d '{ "mode": "chat", "visitorId": "user-123", "messages": [ { "id": "msg-1", "role": "user", "parts": [{ "type": "text", "text": "What are your hours?" }] } ] }'The response is an AI SDK UI message stream. Read
X-Conversation-Idfrom response headers and pass it on the next request to continue the same conversation.5. (Optional) Add voice
Check availability, start a session, connect to the returned WebSocket URL, and stream PCM audio. See the voice section below for the full protocol.
Chat API reference
POST https://losono.xorora.com/api/agents/{agentId}/chat
Request body
{
"mode": "chat",
"visitorId": "optional-stable-visitor-id",
"conversationId": "optional-existing-conversation-uuid",
"messages": [
{
"id": "unique-message-id",
"role": "user",
"parts": [{ "type": "text", "text": "Hello" }]
}
]
}| Field | Required | Notes |
|---|---|---|
mode | Yes* | Must be "chat" for production. Default "playground" is dashboard-only (session auth). |
messages | Yes | Non-empty array. Last user message is used for RAG retrieval. |
conversationId | No | Omit on first message; reuse value from X-Conversation-Id header afterward. |
visitorId | No | Stable ID for analytics and conversation grouping in logs. |
Response
- Body: AI SDK UI message stream (SSE-compatible)
- Header:
X-Conversation-Id: {uuid}
Node.js — server-side chat (no streaming UI)
const AGENT_ID = "550e8400-e29b-41d4-a716-446655440000";
const API_KEY = process.env.LOSONO_API_KEY;
async function askLosono(userText, conversationId) {
const response = await fetch(
`https://losono.xorora.com/api/agents/${AGENT_ID}/chat`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
mode: "chat",
conversationId,
visitorId: "server-job-1",
messages: [
{
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: userText }],
},
],
}),
},
);
if (!response.ok) {
throw new Error(`Chat failed: ${response.status}`);
}
const nextConversationId =
response.headers.get("X-Conversation-Id") ?? conversationId;
// Parse AI SDK stream — use @ai-sdk/ui-utils or your preferred parser
const text = await response.text();
return { text, conversationId: nextConversationId };
}React — streaming with AI SDK
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
const AGENT_ID = "550e8400-e29b-41d4-a716-446655440000";
const API_KEY = process.env.NEXT_PUBLIC_LOSONO_KEY; // only if proxied — prefer a server route
export function CustomChat() {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: `/api/agents/${AGENT_ID}/chat`,
headers: { Authorization: `Bearer ${API_KEY}` },
body: { mode: "chat", visitorId: "web-user-1" },
}),
});
// Render messages and an input that calls sendMessage({ text: "..." })
}Recommended pattern: expose a thin API route on your server that attaches the Bearer token, so the key never ships to browsers.
Voice API reference
Voice requires Pro, voice enabled on the agent, and ?mode=deploy on all voice routes. Reference implementation: src/components/voice/agent-voice.tsx.
1. Check availability
GET https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/voice?mode=deploy&apiKey=losono_sk_...&visitorId=optional // 200 OK { "voiceAvailable": true, "mode": "deploy" } // 403 — voice not available on this plan/agent { "voiceAvailable": false, "reason": "...", "code": "voice_unavailable" }2. Start a session
POST https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/voice?mode=deploy&apiKey=losono_sk_... Content-Type: application/json {} // 200 OK { "conversationId": "uuid", "preview": { "userPrompt": "...", "context": [...] }, "wsUrl": "wss://generativelanguage.googleapis.com/ws/...?access_token=..." }You can pass the API key as
Authorization: Bearer losono_sk_...instead of theapiKeyquery parameter.3. Connect to WebSocket and send setup
Open
wsUrlin a WebSocket client. On open, send an empty setup frame (system prompt is locked in the ephemeral token):{ "setup": {} }Wait for
setupCompletein the first server message before streaming audio.4. Stream microphone audio
Send 16 kHz PCM chunks as base64 inside Gemini realtime input frames:
{ "realtimeInput": { "audio": { "mimeType": "audio/pcm;rate=16000", "data": "<base64-encoded-int16-pcm>" } } }Losono ships AudioWorklet processors at
/audio-worklets/capture-processor.jsand/audio-worklets/playback-processor.jsfor browser clients. Playback audio arrives as base64 PCM at 24 kHz in server messages.5. Handle server events
Parse Gemini Live WebSocket messages for:
inputTranscription/outputTranscription— show live captionsmodelTurn.parts[].inlineData— assistant audio (base64 PCM)interrupted/turnComplete— turn boundaries
6. Persist transcripts (optional)
PATCH https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/voice?mode=deploy&apiKey=losono_sk_... Content-Type: application/json { "action": "transcript", "conversationId": "uuid-from-session", "role": "user", "text": "Transcribed user speech" }7. End session and record usage
PATCH https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/voice?mode=deploy&apiKey=losono_sk_... Content-Type: application/json { "action": "complete", "conversationId": "uuid-from-session", "sessionStartedAt": 1710000000000 }Call this when the user hangs up so voice minutes are billed correctly.
Lead tracking
Capture form submissions and click events from your existing website without replacing your UI. Data appears in the dashboard and can sync to Sales CRM.
What gets captured
- External form submissions — registered forms (validated against a schema you define) or freeform key-value payloads from any HTML form or server handler.
- Click and behavior events — individual interactions (e.g. brochure downloads, product views) grouped into visitor sessions with a 30-minute inactivity window.
Where data appears
- Forms page — external form definitions, submissions table, and CRM field mapping for form fields.
- Tracking page — script snippet, sessions list, event log, and manual session export to CRM.
CRM export behavior
- Pre-chat and external form submissions sync to Sales CRM automatically on submit when CRM is connected and field mapping is complete.
- Tracking sessions export to CRM manually from the Tracking page (bulk export and retry, same pattern as form exports).
Prerequisites
- Agent is published.
- For browser calls from your domain: configure Allowed origins on the deploy page (same as embed iframe chat).
- For server-side form POST: create an API key on the deploy page.
track.js setup
Add lead tracking to any page with a single script tag. No API key is exposed in HTML — browser requests authenticate via allowed origins.
1. Publish the agent
Same as the embed path — tracking endpoints reject traffic until the agent is published.
2. Register external forms (optional)
On the agent Forms page, create registered forms with field schemas. Skip this step if you only need freeform submissions.
3. Configure allowed origins
On the deploy page, add your site origins (e.g.
https://www.example.com). Browser calls fromtrack.jsrequire a matchingOriginheader.4. Copy the track.js snippet
From the agent Tracking page, copy the script tag:
<script src="https://losono.xorora.com/track.js" data-agent="your-agent-slug"></script>
5. Add the script before </body>
Paste on every page where you want click tracking or declarative form capture. The loader resolves your agent slug via
GET /api/public/agents/{slug}, then loads tracking limits and registered forms fromGET /api/agents/{agentId}/track/config— no UUID in the snippet.6. Verify in the dashboard
Trigger a test click or form submit, then check the Tracking page for new sessions and events.
Declarative HTML
Click tracking
<!-- Declarative click tracking -->
<a href="/brochure.pdf" data-losono-track="document_open"
data-losono-props='{"documentId":"brochure-2024"}'>
Download brochure
</a>Registered form capture
<!-- Declarative form capture (registered form slug) --> <form data-losono-form="contact"> <input name="email" type="email" required /> <input name="name" type="text" /> <button type="submit">Send</button> </form>
Form capture modes
By default, data-losono-form intercepts the submit event (preventDefault) and sends data to Losono only — your native form POST or email handler will not run. Listen for losono:submitted or losono:submit-error on the form element for client-side feedback.
To capture in Losono and keep native browser submission (e.g. Formspree, Netlify Forms), add data-losono-form-mode="dual":
Dual capture (Losono + native submit)
<form data-losono-form="contact" data-losono-form-mode="dual" action="/thanks" method="post"> <input name="email" type="email" required /> <button type="submit">Send</button> </form>
Programmatic API
After track.js loads, use the global window.Losono object:
// Programmatic API (after track.js loads)
Losono.track("product_view", { productId: "sku-123", name: "Widget Pro" });
Losono.identify({ email: "user@example.com", name: "Jane" });
Losono.submitForm({ email: "user@example.com", message: "Hello" });Losono.identify() stores traits in localStorage (key losono_traits) alongside losono_visitor_id. Subsequent events and form submissions include those traits automatically.
Server-side form webhook
For backend form handlers, POST directly to the freeform form API with a Bearer token — copy the webhook URL and API key from the Forms page:
POST https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/forms/submit Authorization: Bearer losono_sk_...
Click events API
Ingest click and behavior events. Events are grouped into sessions server-side — clients do not manage session IDs.
POST https://losono.xorora.com/api/agents/{agentId}/track
Tracking config
Fetch limits, registered form schemas, and known event names (for dashboard validation) via:
GET https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/track/config
track.js loads this after resolving the agent slug. Dashboard owners can also call this endpoint while signed in.
Single event
{
"visitorId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"event": "document_open",
"properties": {
"documentId": "brochure-2024",
"title": "Product brochure"
},
"pageUrl": "https://client.com/products",
"timestamp": "2026-06-30T12:00:00Z"
}Batch events
{
"visitorId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"referrer": "https://google.com",
"events": [
{
"event": "page_view",
"pageUrl": "https://client.com/pricing"
},
{
"event": "cta_click",
"properties": { "label": "Start trial" }
}
]
}Batch size is capped at 20 events per request. Each event's properties object must be at most 8 KB when serialized.
Request fields
| Field | Required | Notes |
|---|---|---|
visitorId | Yes | Stable UUID; track.js persists this in losono_visitor_id |
event | Yes* | Event name for single-event body; omit when using events array |
events | No | Batch alternative to top-level event |
properties | No | Arbitrary JSON object (max 8 KB per event) |
pageUrl | No | Page where the event occurred |
timestamp | No | ISO 8601 client timestamp |
referrer | No | Stored on new sessions only |
Response
{
"sessionId": "uuid",
"eventIds": ["uuid", "uuid"],
"eventCount": 12
}Session behavior
Losono finds an open session for (agentId, visitorId) where lastActivityAt is within the last 30 minutes. If none exists, a new session is created with landing page and referrer. Each ingested event increments eventCount and updates the session summary. Sessions are stored only — no CRM call on ingest.
Auth and rate limits
- Browser:
Originmust match an allowed origin on the agent. CORS headers are returned on success. - Server:
Authorization: Bearer losono_sk_...for batch ingest from backends. - Rate limit: 100 events per visitor per minute. Exceeding returns
429 rate_limited.
External forms API
Capture leads from HTML forms on your site or from server-side handlers. Registered forms validate against dashboard schemas; freeform accepts any key-value payload.
Registered forms
Create the form in the dashboard first, then submit to:
POST https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/forms/contact/submit
Use the form slug or UUID as formId. With track.js, set data-losono-form="contact" on the <form> element — the script intercepts submit and POSTs to this endpoint. Field validation matches the registered schema.
Request body
{
"visitorId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"responses": {
"email": "jane@example.com",
"name": "Jane Doe"
},
"pageUrl": "https://example.com/contact",
"metadata": { "utm_source": "google" }
}Response
{
"submissionId": "uuid",
"formId": "uuid",
"submitted": true
}Freeform submissions
Accept any responses object without a pre-registered schema:
POST https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/forms/submit
curl — server-side freeform submit
curl -X POST "https://losono.xorora.com/api/agents/550e8400-e29b-41d4-a716-446655440000/forms/submit" \
-H "Authorization: Bearer losono_sk_..." \
-H "Content-Type: application/json" \
-d '{
"visitorId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"responses": { "email": "jane@example.com", "company": "Acme" },
"pageUrl": "https://example.com/contact",
"formName": "Contact page"
}'Response
{
"submissionId": "uuid",
"submitted": true
}CRM field mapping
- Registered forms: map each form field to CRM columns on the Forms page ( namespaced keys like
external:{formId}:{fieldId}). - Freeform: common keys such as
email,name, andphoneauto-map via normalized label matching; configure explicit mappings for other keys on the Forms page. - Submissions sync to Sales CRM automatically on success when CRM is connected and mapping is complete (same as pre-chat forms).
Sales CRM sync
Developer-oriented summary of how leads reach your CRM. OAuth and field mapping UI live in the dashboard.
| Source | Sync trigger | Dashboard |
|---|---|---|
| Pre-chat form | Auto on submit | Forms |
| External form | Auto on submit | Forms |
| Tracking session | Manual export | Tracking |
Idempotency
Export records use Losono UUIDs as idempotency keys (leadSource + leadSourceId). Retrying a failed export does not create duplicate CRM records when the original sync succeeded.
Session export fields
Session exports expose virtual fields you can map to CRM columns:
session:visitor_id,session:landing_page,session:referrersession:event_count,session:last_event,session:events_summary
Authentication
How Losono validates requests depends on the integration path. Track and form endpoints share the same deploy access rules as chat.
Embed widget
No API key. Chat and voice requests from the hosted iframe authenticate via the Referer header (https://losono.xorora.com/embed/{slug}).
Custom API — Bearer token (recommended)
Authorization: Bearer losono_sk_...
Create keys on the agent deploy page. Keys use the losono_sk_ prefix and are stored hashed server-side. Revoke compromised keys immediately from the dashboard.
Custom API — allowed origins (browser only)
If you configure Allowed origins on the deploy page and call chat from a browser whose Origin matches, requests may succeed without a Bearer token. This is intended for same-site frontends you control — prefer server-side keys for anything sensitive.
Allowed entries can be full origins (https://app.example.com), hostnames, or wildcards like *.example.com. Empty list = allow all origins.
Voice API key placement
Voice routes accept the key as either:
Authorization: Bearer losono_sk_...header, or?apiKey=losono_sk_...query parameter (useful for WebSocket-adjacent fetch calls)
Lead tracking auth
- Browser (track.js): No API key in HTML. Requests authenticate via allowed origins on the agent — the same mechanism as embed iframe chat.
track.jsresolves the agent slug throughGET https://losono.xorora.com/api/public/agents/your-agent-slug. - Server-side form POST:
Authorization: Bearer losono_sk_...onPOST /api/agents/{agentId}/forms/submit(and registered form endpoints). - visitorId: Client-generated UUID persisted in localStorage under
losono_visitor_id. Shared with the embed widget for cross-surface visitor correlation.
Message format
Chat uses Vercel AI SDK UI messages. Each message has a role and a parts array.
{
"id": "unique-string",
"role": "user" | "assistant" | "system",
"parts": [
{ "type": "text", "text": "Message content" }
]
}Send the full conversation history on every chat request. The API extracts text from the last user message for RAG retrieval and persists both user and assistant turns when streaming completes.
Streaming responses follow the AI SDK UI message stream protocol. Use @ai-sdk/react with DefaultChatTransport or parse SSE events manually.
Error codes
Common JSON error bodies returned by deploy, track, and form APIs.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or invalid auth (API key, origin, or referer) |
| 401 | invalid_api_key | Bearer token is not a valid active key for this agent |
| 403 | agent_not_published | Agent exists but is not published |
| 400 | messages_required | Chat body missing messages array |
| 400 | user_message_required | No non-empty user text in messages |
| 400 | invalid_mode | mode must be chat or playground |
| 400 | invalid_json | Malformed JSON request body (track/form endpoints) |
| 400 | visitor_id_required | Missing visitorId on track or form submit |
| 400 | validation_failed | Invalid event payload, batch size, properties size, or registered form field validation |
| 404 | form_not_found | Unknown form slug or UUID on registered form submit |
| 429 | rate_limited | More than 100 track events per visitor per minute |
| 403 | voice_unavailable | Pro/voice not enabled or not allowed for this agent |
| 503 | gemini_not_configured | Server missing GOOGLE_GENERATIVE_AI_API_KEY |
| 500 | conversation_failed | Could not create or load conversation |
| 500 | retrieval_failed | RAG retrieval error |