Streaming
The same request as /api/query, answered as Server-Sent Events while the run works: the steps it takes, the sources it finds and reads, its reasoning, and then the final envelope.
01Endpoint
Authentication and request body are the same as for /api/query: an API key, and {"query": "…"}. Each call starts a new run and uses one research run from your monthly allowance.
The response has Content-Type: text/event-stream and an X-Request-Id header carrying the call's request_id. Each event is an event: line naming the type, a data: line holding one JSON object, and a blank line.
event: start
data: {"request_id":"3f1c9a2e-7b4d-4e8a-9c61-2d5f0b8e4a17","query":"Compare lithium-ion and sodium-ion batteries for grid storage."}
event: step
data: {"phase":"analyzing_query","label":"Analyzing query intent, constraints, and desired output shape","message":"Analyzing query intent, constraints, and desired output shape","progress":0.03,"meta":{}}
The event type is only on the event: line; it is not repeated inside the JSON.
02Event types
| Event | When it is sent | Data |
|---|---|---|
| start | Once, first, when the run begins. | request_id, query |
| step | Each time the run enters or reports on a phase. | phase, label, message, progress, meta |
| source | When a source is found, fetched, or analysed. | id, stage, title, url, domain, published_date, and for fetches word_count and status |
| reasoning | While the run explains what it is doing, in chunks. | step, step_id, stage, chunk |
| answer_delta | Once, just before final, when there is an answer. | chunk |
| error | When the call is refused, or the stream itself fails. | code, message, request_id |
| final | Always last. The stream closes after it. | The response envelope |
New phases, fields and event types may be added. Ignore what you don't recognise.
step
phase is a machine name; label is a short description to show a reader, and message a longer one. progress runs from 0 to 1. meta carries phase-specific details. The same phase can be reported more than once.
A run currently moves through these phases, in this order: analyzing_query, planning, searching, deduping, scraping, screening, selecting_top10, deep_analyzing_top10, generating_citations, synthesizing, complete. A step with phase top10_selected lists the sources picked for close reading in meta.source_ids, and a step with phase error reports a failure inside the run.
event: step
data: {"phase":"searching","label":"Searching web + academic databases...","message":"Searching trusted sources across targeted query variants","progress":0.15,"meta":{"strategy":{},"source_mode":"auto"}}
source
stage is discovered when a search returns the source, scraped when the run has fetched it, and analyzed when it has been read closely. Only discovered events carry title, url and domain; later stages carry the same id with the other fields null, so keep a map from id to the discovered source. Each source is announced as discovered once.
On scraped events, status is success, failed or low_quality, and word_count is the length of the text extracted. The id values match sources[].id in the final envelope.
event: source
data: {"id":"6a0d2c4e-1f3b-4c5d-8e9f-0a1b2c3d4e5f","title":"…","url":"https://…","domain":"…","published_date":null,"stage":"discovered"}
event: source
data: {"id":"6a0d2c4e-1f3b-4c5d-8e9f-0a1b2c3d4e5f","title":null,"url":null,"domain":null,"published_date":null,"stage":"scraped","word_count":3120,"status":"success"}
event: source
data: {"id":"6a0d2c4e-1f3b-4c5d-8e9f-0a1b2c3d4e5f","title":null,"url":null,"domain":null,"published_date":null,"stage":"analyzed"}
reasoning
The run narrates each substantial phase. A narration opens with stage: "start", continues with stage: "chunk" events whose chunk text you append in order, and closes with stage: "done". step is the phase being narrated and step_id groups the chunks of one narration.
event: reasoning
data: {"step":"searching","step_id":"searching","stage":"chunk","chunk":"Starting with grid-scale deployments, since cost per cycle matters more there than energy density …"}
answer_delta
The answer arrives in one piece, not token by token. chunk is the same text as answer in the final event. It is not sent when there is no answer.
error
Sent when the call is refused before the run starts (a key problem, or no research runs left this month), when the run cannot be started, or when the stream fails part-way. A final event with status: "error" always follows. The quota_exceeded error also carries limit, used, plan and period.
event: error
data: {"code":"invalid_api_key","message":"API key is invalid.","request_id":"3f1c9a2e-7b4d-4e8a-9c61-2d5f0b8e4a17"}
event: final
data: {"request_id":"3f1c9a2e-7b4d-4e8a-9c61-2d5f0b8e4a17","status":"error","answer":null,"sources":[],"usage":{},"errors":[{"code":"invalid_api_key","message":"API key is invalid."}]}
Failures inside a run that has started are not error events. They show up as a step with phase error, and in the errors array of final.
final
The last event, always. Its data is the same envelope /api/query returns. Treat it as the result, and check its status.
event: final
data: {"request_id":"3f1c9a2e-7b4d-4e8a-9c61-2d5f0b8e4a17","status":"ok","answer":"…","sources":[…],"usage":{"latency_ms":98410,"tokens_in":16,"tokens_out":790},"errors":[]}
03HTTP status and disconnects
A refused call does not always get a matching HTTP status on this endpoint:
- Key problems (
missing_api_key,invalid_api_key,key_inactive,identity_db_unavailable) arrive aserrorandfinalevents on an HTTP200stream. quota_exceededarrives as the same two events with HTTP429.- A body that fails validation gets HTTP
422with a JSON body, not a stream.
So read the stream to its final event and decide on its status, whatever the HTTP code.
Closing the connection before final stops the run. The research run it used is not given back, and the call is not recorded in your usage. There is no way to resume a stream: a new request is a new run.
04Examples
cURL
-N stops curl buffering, so each event prints as it arrives.
curl -N https://researchanything.ai/api/query/stream \
-H "Authorization: Bearer $ESSARION_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"query": "Compare lithium-ion and sodium-ion batteries for grid storage."}'
Node — fetch
The browser EventSource API only makes GET requests, so read the POST response body yourself.
async function* streamQuery(query) {
const res = await fetch("https://researchanything.ai/api/query/stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ESSARION_KEY}`,
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
body: JSON.stringify({ query }),
});
if (res.status === 422) throw new Error(JSON.stringify(await res.json()));
const decoder = new TextDecoder();
let buffer = "";
for await (const bytes of res.body) {
buffer += decoder.decode(bytes, { stream: true });
let end;
while ((end = buffer.indexOf("\n\n")) !== -1) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
let event = "message";
let data = "";
for (const line of block.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (data) yield { event, data: JSON.parse(data) };
}
}
}
for await (const { event, data } of streamQuery("Compare lithium-ion and sodium-ion batteries for grid storage.")) {
if (event === "step") console.log("step:", data.label);
if (event === "final") {
if (data.status !== "ok") throw new Error(JSON.stringify(data.errors));
console.log(data.answer);
}
}
Python — httpx
import json
import os
import httpx
def stream_query(question: str):
headers = {
"Authorization": f"Bearer {os.environ['ESSARION_KEY']}",
"Accept": "text/event-stream",
}
timeout = httpx.Timeout(10.0, read=None)
with httpx.stream("POST", "https://researchanything.ai/api/query/stream",
headers=headers, json={"query": question}, timeout=timeout) as resp:
event, data = "message", []
for line in resp.iter_lines():
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data.append(line[5:].strip())
elif line == "" and data:
yield event, json.loads("".join(data))
event, data = "message", []
for event, data in stream_query("Compare lithium-ion and sodium-ion batteries for grid storage."):
if event == "step":
print("step:", data["label"])
elif event == "final":
print(data["status"], data["answer"])
For a fuller treatment, including relaying the stream to a browser through your own server, see Streaming with SSE.