DocsAPIStreaming

Streaming with SSE.

Long-running queries return progressive events as they unfold. Watch discovery, scraping, analysis, and reasoning happen in real time.

§ 01Endpoint

POST/api/v1/query/stream

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.

EventWhen it firesNotable fields
startOnce, immediately after the request is accepted.request_id, created_at
messageAs the engine emits status text — sub-query plans, progress notes.text, phase
sourcesEach time a batch of sources advances to a new stage.stage, items[]
reasoningAs the engine generates internal reasoning chunks.chunk, step_id
finalOnce, when the synthesized answer is ready.answer, sources, usage
errorOn any failure that aborts the run.code, message

start

sse
data: {"event":"start","request_id":"req_01HXYZ","created_at":"2026-05-01T14:22:08Z"}

message

sse
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.

sse
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

sse
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.

sse
data: {"event":"final","request_id":"req_01HXYZ","answer":"...","sources":[...],"usage":{"tokens_in":612,"tokens_out":2240,"latency_ms":24800}}

error

sse
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.

TipKeep the connection open. Don't close it after the first event. SSE clients should reconnect on transient transport failures (network blip, proxy churn) — re-issue the POST with the same body. The new run gets a new 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
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.

node
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

python
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.