DocsAPIStreaming

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

POSThttps://researchanything.ai/api/query/stream

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.

SSE
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

EventWhen it is sentData
startOnce, first, when the run begins.request_id, query
stepEach time the run enters or reports on a phase.phase, label, message, progress, meta
sourceWhen a source is found, fetched, or analysed.id, stage, title, url, domain, published_date, and for fetches word_count and status
reasoningWhile the run explains what it is doing, in chunks.step, step_id, stage, chunk
answer_deltaOnce, just before final, when there is an answer.chunk
errorWhen the call is refused, or the stream itself fails.code, message, request_id
finalAlways 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.

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

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

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

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

SSE
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:

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

Node
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

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

Can’t find what you need?

Write to us and a person will answer.