Streaming with SSE.
Long-running queries return progressive events as they unfold. Watch discovery, scraping, analysis, and reasoning happen in real time.
§ 01Endpoint
Same auth and request body as /api/v1/query. The difference is the response — a text/event-stream connection that emits Server-Sent Events as the run unfolds.
Send Accept: text/event-stream on the request. The server responds with Content-Type: text/event-stream and a chunked transfer-encoded body. Each event is a data:-prefixed line containing a JSON object.
§ 02Event types
Six event types cover the full run lifecycle. Every event has an event field and a request_id. Beyond that, the payload depends on the type.
| Event | When it fires | Notable fields |
|---|---|---|
| start | Once, immediately after the request is accepted. | request_id, created_at |
| message | As the engine emits status text — sub-query plans, progress notes. | text, phase |
| sources | Each time a batch of sources advances to a new stage. | stage, items[] |
| reasoning | As the engine generates internal reasoning chunks. | chunk, step_id |
| final | Once, when the synthesized answer is ready. | answer, sources, usage |
| error | On any failure that aborts the run. | code, message |
start
data: {"event":"start","request_id":"req_01HXYZ","created_at":"2026-05-01T14:22:08Z"}
message
data: {"event":"message","request_id":"req_01HXYZ","phase":"plan","text":"Decomposing into 4 sub-queries..."}
sources
The stage field walks through the source pipeline. discovered is the raw search hit, scraped means content has been fetched, analyzed means it has been screened and scored.
data: {"event":"sources","request_id":"req_01HXYZ","stage":"discovered","items":[
{"url":"https://...","title":"...","domain":"..."}
]}
data: {"event":"sources","request_id":"req_01HXYZ","stage":"scraped","items":[
{"url":"https://...","bytes":48211,"status":200}
]}
data: {"event":"sources","request_id":"req_01HXYZ","stage":"analyzed","items":[
{"url":"https://...","relevance":0.91,"credibility":0.84,"keep":true}
]}
reasoning
data: {"event":"reasoning","request_id":"req_01HXYZ","step_id":"step_03","chunk":"Comparing the two regimes on scope of personal data..."}
final
The terminal event. After final, the connection closes cleanly. Treat it as your signal to stop reading and resolve.
data: {"event":"final","request_id":"req_01HXYZ","answer":"...","sources":[...],"usage":{"tokens_in":612,"tokens_out":2240,"latency_ms":24800}}
error
data: {"event":"error","request_id":"req_01HXYZ","code":"UPSTREAM_TIMEOUT","message":"Engine timed out after 300s"}
§ 03Timeouts
The stream has two timeouts. The initial response timeout is 180 seconds — if the gateway can't establish the engine connection within that window, you get a 504 before any events fire. Once events start flowing, the stream timeout is 300 seconds — the longest the run is allowed to take from start to final.
request_id; if you need idempotency, dedupe on your own request id in metadata.§ 04Examples
curl
Pass --no-buffer so curl prints each event as it arrives instead of waiting for the connection to close.
curl --no-buffer https://api.essarion.com/api/v1/query/stream \
-H "Authorization: Bearer $ESSARION_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"query": "Compare lithium-ion and solid-state batteries for grid storage."}'
Node — fetch streaming
The browser EventSource API only does GET; for POST-based SSE the cleanest server-side approach is fetch with a streaming reader.
const res = await fetch("https://api.essarion.com/api/v1/query/stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ESSARION_KEY}`,
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
body: JSON.stringify({ query: "Compare lithium-ion and solid-state batteries." }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) !== -1) {
const raw = buf.slice(0, idx).trim();
buf = buf.slice(idx + 2);
if (!raw.startsWith("data:")) continue;
const event = JSON.parse(raw.slice(5).trim());
if (event.event === "final") {
console.log("DONE:", event.answer);
return;
}
if (event.event === "error") {
throw new Error(`${event.code}: ${event.message}`);
}
console.log(event.event, event);
}
}
Python — httpx
import os, json, httpx
async def stream_query(question: str):
async with httpx.AsyncClient(timeout=350.0) as client:
async with client.stream(
"POST",
"https://api.essarion.com/api/v1/query/stream",
headers={
"Authorization": f"Bearer {os.environ['ESSARION_KEY']}",
"Accept": "text/event-stream",
},
json={"query": question},
) as resp:
async for line in resp.aiter_lines():
if not line.startswith("data:"):
continue
event = json.loads(line[5:].strip())
if event["event"] == "final":
return event
if event["event"] == "error":
raise RuntimeError(f"{event['code']}: {event['message']}")
print(event["event"], event)
The requests library can also stream — pass stream=True and iterate response.iter_lines() — but for any production workload async httpx is the recommended path.