SSE Streaming
How to consume Server-Sent Events from the AI tutor chat endpoint.
The POST /chat endpoint streams the AI tutor's response in real time using Server-Sent Events (SSE). This allows your application to display text as it's generated, providing a conversational experience.
Event Types#
The stream emits three event types:
chunk β Text delta#
Fired multiple times as the tutor generates its response. Each chunk contains a fragment of text.
event: chunk
data: {"type":"chunk","content":"μλ
"}
event: chunk
data: {"type":"chunk","content":"νμΈμ! "}
event: chunk
data: {"type":"chunk","content":"How can I help you today?"}
Concatenate all content values to build the full response.
done β Stream complete#
Fired once when the tutor finishes generating. Contains metadata about the response.
event: done
data: {"type":"done","conversation_id":"clxyz123...","corrections":[],"usage":{"input_tokens":42,"output_tokens":156},"model":{"provider":"anthropic","id":"claude-sonnet-4-20250514"},"title":"Getting Started with Korean"}
| Field | Description |
|---|---|
conversation_id | The conversation ID (use for follow-up messages) |
corrections | Grammar/spelling corrections the tutor detected |
usage | Token counts (input_tokens, output_tokens) |
model | Provider and model ID used for the response |
title | Conversation title (only on first message) |
usage_warning | Quota warning if nearing limit (optional) |
error β Stream error#
Fired if an error occurs during generation. The stream closes after this event.
event: error
data: {"type":"error","message":"Stream failed","request_id":"req_abc","retryable":true,"reason":"rate_limit"}
| Field | Description |
|---|---|
message | Human-readable error description |
request_id | Request ID for debugging |
retryable | Whether the request can be safely retried |
reason | Error category: rate_limit, timeout, internal |
Consuming SSE in TypeScript#
Using the Fetch API#
async function chat(message: string, conversationId?: string) {
const response = await fetch("https://api.chamelingo.com/api/v1/chat", {
method: "POST",
headers: {
"Authorization": "Bearer ck_live_YOUR_KEY_HERE",
"Content-Type": "application/json",
},
body: JSON.stringify({
message,
...(conversationId && { conversation_id: conversationId }),
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error.message);
}
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let fullText = "";
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process complete SSE messages
const lines = buffer.split("\n");
buffer = lines.pop() ?? ""; // Keep incomplete line in buffer
let eventType = "";
for (const line of lines) {
if (line.startsWith("event: ")) {
eventType = line.slice(7);
} else if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
switch (eventType) {
case "chunk":
fullText += data.content;
process.stdout.write(data.content); // Stream to terminal
break;
case "done":
console.log("\n\nConversation:", data.conversation_id);
console.log("Tokens:", data.usage);
return { text: fullText, ...data };
case "error":
throw new Error(`Stream error: ${data.message}`);
}
}
}
}
}
// Start a new conversation
const result = await chat("How do I say 'hello' in Korean?");
// Continue the conversation
await chat("And how about 'goodbye'?", result.conversation_id);
Using EventSource (browser)#
// Note: EventSource only supports GET, so use fetch for POST + manual parsing
// This example uses the eventsource-parser library for cleaner parsing
import { createParser } from "eventsource-parser";
async function streamChat(message: string, onChunk: (text: string) => void) {
const response = await fetch("https://api.chamelingo.com/api/v1/chat", {
method: "POST",
headers: {
"Authorization": "Bearer ck_live_YOUR_KEY_HERE",
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});
const parser = createParser((event) => {
if (event.type === "event") {
const data = JSON.parse(event.data);
if (event.event === "chunk") {
onChunk(data.content);
}
}
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
parser.feed(decoder.decode(value, { stream: true }));
}
}
// Usage in a React component
streamChat("Teach me Korean numbers", (chunk) => {
setResponse((prev) => prev + chunk);
});
Consuming SSE with cURL#
curl -N -X POST https://api.chamelingo.com/api/v1/chat \
-H "Authorization: Bearer ck_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"message": "How do I count in Korean?"}'
The -N flag disables buffering so you see events in real time.
Usage Warnings#
When you're approaching your subscription quota, the done event includes a usage_warning field:
{
"type": "done",
"conversation_id": "clxyz...",
"usage_warning": {
"feature": "ai_text_messages",
"used": 490,
"limit": 500,
"remaining": 10,
"message": "You have 10 messages remaining today."
}
}
Check this field proactively to notify users before they hit quota limits. Use GET /usage for a full quota breakdown.
Connection Handling#
- Timeouts: The stream has a 60-second inactivity timeout. If no events arrive within 60 seconds, the connection closes.
- Client disconnects: If you close the connection, the server detects it and stops generation.
- Retries: If you receive an
errorevent withretryable: true, wait a few seconds and retry the request. Don't reconnect to the same stream β start a newPOST /chatrequest.
Don't retry on retryable: false errors β these indicate a problem with your request (invalid message, missing scope, etc.) that won't be resolved by retrying.