Streaming guide
Both the Customer Chat API and Management Chat API return Server-Sent Events (SSE) streams when you send a message. This guide covers the stream format and provides consumption examples in multiple languages.
Event format
Each SSE event consists of an id: line and a data: line, separated by a blank line:
| 1 | id: 1 |
| 2 | data: {"type":"delta","text":"Our"} |
| 3 | |
| 4 | id: 2 |
| 5 | data: {"type":"delta","text":" business hours are 9am–5pm."} |
| 6 | |
| 7 | id: 3 |
| 8 | data: [DONE] |
Event types
| Event | data: payload | Description |
|---|---|---|
| Delta | {"type":"delta","text":"..."} | Incremental text chunk — append to the response |
| Status | {"type":"status","status":"escalated"} | Conversation state change (e.g. escalated to human) |
| Error | {"type":"error","message":"..."} | Mid-stream error (rare) |
| Done | [DONE] | Stream complete — no more events will follow |
Event IDs
Every event includes a monotonically incrementing id: field (starting at 1). Use this for completeness checking (verify no events were lost) and debugging.
Pre-stream errors
If the request fails before streaming begins (auth, billing, validation), the response is a standard JSON error with an appropriate HTTP status code — not an SSE stream. Always check the response status code before attempting to read the stream.
Code examples
All examples follow the same pattern: POST a message, consume the SSE stream, and collect the full response text.
| 1 | const BASE_URL = "https://lexey.ai/api/v1/customer"; |
| 2 | |
| 3 | async function sendMessage(conversationId: string, message: string): Promise<string> { |
| 4 | const res = await fetch( |
| 5 | `${BASE_URL}/conversations/${conversationId}/messages`, |
| 6 | { |
| 7 | method: "POST", |
| 8 | headers: { |
| 9 | Authorization: `Bearer ${API_KEY}`, |
| 10 | "Content-Type": "application/json", |
| 11 | }, |
| 12 | body: JSON.stringify({ message }), |
| 13 | }, |
| 14 | ); |
| 15 | |
| 16 | if (!res.ok) { |
| 17 | const error = await res.json(); |
| 18 | throw new Error(error.error); |
| 19 | } |
| 20 | |
| 21 | const reader = res.body!.getReader(); |
| 22 | const decoder = new TextDecoder(); |
| 23 | let fullText = ""; |
| 24 | let buffer = ""; |
| 25 | |
| 26 | while (true) { |
| 27 | const { done, value } = await reader.read(); |
| 28 | if (done) break; |
| 29 | |
| 30 | buffer += decoder.decode(value, { stream: true }); |
| 31 | const parts = buffer.split("\n\n"); |
| 32 | buffer = parts.pop()!; |
| 33 | |
| 34 | for (const part of parts) { |
| 35 | const dataLine = part.split("\n").find((l) => l.startsWith("data: ")); |
| 36 | if (!dataLine) continue; |
| 37 | const data = dataLine.slice(6); |
| 38 | if (data === "[DONE]") return fullText; |
| 39 | |
| 40 | try { |
| 41 | const event = JSON.parse(data); |
| 42 | if (event.type === "delta") fullText += event.text; |
| 43 | else if (event.type === "error") throw new Error(event.message); |
| 44 | } catch (e) { |
| 45 | if (e instanceof SyntaxError) continue; // skip malformed lines |
| 46 | throw e; |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | return fullText; |
| 51 | } |
Edge cases
- Chunked reads may split events — TCP chunks don't align with SSE event boundaries. Always buffer incoming data and split on
\n\nto extract complete events. - Error mid-stream — If the server encounters an error after streaming has begun, it emits an
errorevent followed by[DONE]. - Connection timeout — Streaming requests have a 120-second timeout. If the connection drops before
[DONE], treat the collected text as partial. - Empty response — If the agent's response is blocked by a safety filter, the stream may contain a single delta with the refusal message, then
[DONE]. This is not an error.