Skip to content

API reference

Streaming events

Every server-sent event the chat stream can send, in order, with each field, plus how to parse the stream safely.

Reference3 min readUpdated 23 October 2026

The chat stream uses plain server-sent events: each event is one line starting with data: followed by a JSON object, then a blank line. There are no event: or id: lines, so read the type field to tell events apart.

Order

  1. meta, always first.
  2. sources, at most once, before any delta, only when knowledge was used.
  3. tool_call and tool_result pairs, each time the agent calls a tool.
  4. delta events with the answer text.
  5. done on success, or error on failure. Exactly one of them, always last.

Events

typeFieldsNotes
metaconversation_id, trace_idThe trace id is a 32-character hex OpenTelemetry id
sourcessources: list of index, id, title, url, filename, source_type, score, snippetindex starts at 1. snippet is up to 240 characters. url is set for web pages, filename for documents
tool_callid, name, inputThe agent is calling an MCP tool with these arguments
tool_resultid, name, outputWhat the tool returned (the model sees up to 2,500 characters of it)
deltacontentThe next piece of the answer. A cached answer arrives as a single delta
doneconversation_id, trace_id, usage; optional cached, timingusage has input_tokens, output_tokens, total_tokens, cost_usd. cached: true on an answer-cache hit; timing when you sent X-Perf-Timing: 1
errorcode, message, request_idThe reply failed after the stream started. See Errors
Example stream
data: {"type": "meta", "conversation_id": "9b1c…", "trace_id": "4f2a9c1e…"}data: {"type": "sources", "sources": [{"index": 1, "id": "…", "title": "Shipping policy", "url": "https://example.com/shipping", "filename": null, "source_type": "web", "score": 0.82, "snippet": "Orders leave our warehouse…"}]}data: {"type": "tool_call", "id": "call_1", "name": "get_order", "input": {"order_id": "10482"}}data: {"type": "tool_result", "id": "call_1", "name": "get_order", "output": "{\"status\": \"shipped\"}"}data: {"type": "delta", "content": "Order 10482 left our warehouse"}data: {"type": "delta", "content": " on Tuesday."}data: {"type": "done", "conversation_id": "9b1c…", "trace_id": "4f2a9c1e…",       "usage": {"input_tokens": 1830, "output_tokens": 64, "total_tokens": 1894, "cost_usd": 0}}

Parsing it

  • Network chunks don't line up with events. Buffer the text, split on a blank line (\n\n), and keep the last, possibly half-received, piece for the next chunk.
  • Ignore lines that don't start with data: , so a future addition can't break your parser.
  • Ignore event types you don't know. New types may be added; existing fields won't change meaning.
  • Treat the stream as failed if it ends without done or error (for example a dropped connection), and show the user a retry.
  • cost_usd is an estimate from the token prices set on the agent, and is 0 when none are set. Your model provider's bill is the source of truth.
stream.mjs
// Node 18+ (fetch and web streams built in)const res = await fetch("https://api.sentientone.ai/v1/chat/stream", {  method: "POST",  headers: {    "Content-Type": "application/json",    "X-Api-Key": process.env.SENTIENTONE_API_KEY,    "X-Agent-Id": process.env.AGENT_ID,    "X-Request-Id": crypto.randomUUID(),  },  body: JSON.stringify({ message: "Where is order 10482?" }),})if (!res.ok) {  // Errors before the stream starts: an HTTP status and one JSON envelope  const { error } = await res.json()  throw new Error(error.code + ": " + error.message + " (ref " + error.request_id + ")")}const reader = res.body.getReader()const decoder = new TextDecoder()let buffer = ""let conversationId = nullwhile (true) {  const { done, value } = await reader.read()  if (done) break  buffer += decoder.decode(value, { stream: true })  const events = buffer.split("\n\n")  buffer = events.pop() // keep a half-received event for the next chunk  for (const raw of events) {    if (!raw.startsWith("data: ")) continue    const event = JSON.parse(raw.slice(6))    switch (event.type) {      case "meta": conversationId = event.conversation_id; break      case "sources": console.log("sources", event.sources.map((s) => s.title)); break      case "tool_call": console.log("calling", event.name, event.input); break      case "delta": process.stdout.write(event.content); break      case "done": console.log("\nusage", event.usage); break      case "error": throw new Error(event.code + ": " + event.message)    }  }}

Citations in the text

Agents with knowledge are told to end their answer with Source: lines: a markdown link for web pages, or the file name for documents. Q&A entries are never cited. If you'd rather render your own citations, use the sources event. See Sources and citations.

Still stuck?

Email us with what you tried and any request id. A person replies.

support@sentientone.ai