POST /v1/research always responds as a Server-Sent Events (SSE) stream. You
receive the model's reasoning, its searches, and the answer as they happen, then a
final event with the complete result.
The stream format
The response has Content-Type: text/event-stream. Each event is a single
data: line containing a JSON object. Every object shares the same envelope:
The stream ends with a literal data: [DONE] line. Stop reading when you see it
(or a terminal research.failed event).
✦
Disable client-side buffering so events surface immediately — e.g. curl's
-N flag, or reading response.body as a stream in
fetch. Bahith already sends X-Accel-Buffering: no.
Event types
research.startedeventoptional
Emitted once at the start. data: query,
reasoning_level, max_iterations.
research.thinking.deltaeventoptional
A fragment of the model's reasoning. data:
iteration, content, sequence.
Suppressed when options.include_thinking is false.
research.tool_call.startedeventoptional
A search/browse tool was invoked. data: tool_id,
tool_name, query, parameters.
research.tool_call.completedeventoptional
A tool finished. data: tool_id,
runtime_ms, success, sources_found.
The research.completed and
research.failed events are terminal — exactly
one of them precedes [DONE].
Consuming the stream
bash
curl -s -N -X POST https://api.bahith.dev/v1/research \ -H "Content-Type: application/json" \ -H "X-API-Key: $BAHITH_API_KEY" \ -d '{ "query": "What is RLHF?" }' | while read -r line; do if [[ "$line" =~ ^data:\ (.*) ]]; then payload="${BASH_REMATCH[1]}" if [ "$payload" = "[DONE]" ]; then break fi type=$(echo "$payload" | jq -r '.type') if [ "$type" = "research.answer.delta" ]; then echo -n "$(echo "$payload" | jq -r '.data.content')" elif [ "$type" = "research.failed" ]; then echo "Error: $(echo "$payload" | jq -r '.data.message')" exit 1 fi fi done
typescript
const res = await fetch("https://api.bahith.dev/v1/research", { method: "POST", headers: { "Content-Type": "application/json", "X-API-Key": "YOUR_API_KEY" }, body: JSON.stringify({ query: "What is RLHF?", reasoning_level: "medium" }),});const reader = res.body.getReader();const decoder = new TextDecoder();let buffer = "";while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const payload = line.slice(6); if (payload === "[DONE]") return; const event = JSON.parse(payload); if (event.type === "research.answer.delta") { process.stdout.write(event.data.content); } else if (event.type === "research.failed") { throw new Error(event.data.message); } }}
python
import jsonimport requestsresp = requests.post( "https://api.bahith.dev/v1/research", headers={"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={"query": "What is RLHF?", "reasoning_level": "medium"}, stream=True,)for line in resp.iter_lines(): if not line: continue payload = line.decode("utf-8").removeprefix("data: ") if payload == "[DONE]": break event = json.loads(payload) if event["type"] == "research.answer.delta": print(event["data"]["content"], end="", flush=True) elif event["type"] == "research.completed": print(f"\n\n{len(event['data']['citations'])} citations") elif event["type"] == "research.failed": raise RuntimeError(event["data"]["message"])
java
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.stream.Stream;public class BahithStreamConsumer { public static void main(String[] args) throws Exception { String json = """ { "query": "What is RLHF?", "reasoning_level": "medium" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.bahith.dev/v1/research")) .header("Content-Type", "application/json") .header("X-API-Key", "YOUR_API_KEY") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) .thenAccept(response -> { try (Stream<String> lines = response.body()) { lines.forEach(line -> { if (line.startsWith("data: ")) { String payload = line.substring(6); if (payload.equals("[DONE]")) { return; } if (payload.contains("\"type\":\"research.answer.delta\"")) { int start = payload.indexOf("\"content\":\"") + 11; int end = payload.indexOf("\"", start); if (start > 10 && end > start) { System.out.print(payload.substring(start, end)); System.out.flush(); } } else if (payload.contains("\"type\":\"research.failed\"")) { int start = payload.indexOf("\"message\":\"") + 11; int end = payload.indexOf("\"", start); if (start > 10 && end > start) { System.err.println("\nError: " + payload.substring(start, end)); } } } }); } }).join(); }}
▲
Buffer partial lines. TCP chunks don't align with event boundaries, so
accumulate bytes and split on newlines (as above) rather than assuming each
read is one complete event.
Handling failures
If research fails mid-stream, you receive a research.failed event before
[DONE] instead of research.completed: