---
title: "Streaming answers"
description: "Receive an answer as server-sent events while it is written, with the status, delta, done and error events, and how failures and costs behave."
canonical: "https://lookio.app/docs/developers/streaming"
updated: "2026-11-08"
---

# Streaming answers

By default, [Ask an assistant](/docs/api/assistants/query-assistant) returns the whole answer as JSON. Set `stream` to `true` to get the answer while it is written, as server-sent events.

```bash
curl -N https://api.lookio.app/v1/assistants/ASSISTANT_ID/query \
  -H "Authorization: Bearer $LOOKIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{ "question": "How long do I have to return an item?", "stream": true }'
```

The response has `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-store` and an `X-Request-Id` header. Each event has a name and one line of JSON:

```text
event: status
data: {"phase":"searching"}

event: delta
data: {"text":"You can return any item "}

event: delta
data: {"text":"within 30 days [1]."}

event: done
data: {"answer":"You can return any item within 30 days [1].","citations":[],"conversation_id":"cnv_p2x8w5r9m3hk","message_id":"msg_k7m3x9p2w8qr","credit_cost":30,"credits_remaining":970,"simulated":false}
```

`citations` is shortened here. It has the same shape as in the JSON answer.

## Events

```mermaid title="The order of the events"
flowchart LR
  ST["status<br/>searching, reading, answering"] --> D["delta<br/>pieces of text"]
  D --> D
  D --> DN["done<br/>full answer, citations, ids, cost"]
  ST -.-> E["error<br/>instead of done"]
  D -.-> E
```

| Event    | Data             | Meaning                                                                                                                                                                                                            |
| -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status` | `phase`          | What Lookio is doing: `searching`, `reading`, then `answering`.                                                                                                                                                    |
| `delta`  | `text`           | The next piece of the answer. Append the pieces in order.                                                                                                                                                          |
| `done`   | The final answer | The end of the stream. The same fields as the JSON answer: `answer`, `citations`, `conversation_id`, `message_id`, `credit_cost`, `credits_remaining` and `simulated`. The two ids are `null` with `store: false`. |
| `error`  | The error format | The answer failed. The data has the format of a failed request: `error` (`type`, `code`, `message`, `docs_url`, sometimes `action_url`) and `request_id`. The stream ends.                                         |

On success, `done` is always last. Use its `answer` as the final text, with its numbered citations.

When there is no other data, the stream sends the comment `: keep-alive` every 15 seconds, for example while an Eco answer starts. It keeps proxies from closing the stream. SSE parsers ignore it.

## Failures

- **Before the first event.** A refusal such as `insufficient_credits`, `insufficient_scope`, `resource_not_found` or `service_unavailable` comes as a normal JSON error with its HTTP status. See [Errors](/docs/developers/errors).
- **After the first event.** The HTTP status is already `200`. The failure comes as an `error` event, then the stream ends. Treat any `error` event as a failed answer.

Read until `done` or `error`. If the stream closes without one of them, the answer failed. Do not use the partial text.

## Conversations and costs

- With `done`, Lookio stores the question and the answer in the person's chat or the API key's conversation, except with `store: false`. `done` has the `conversation_id` and the answer's `message_id`. See [Conversations and end users](/docs/guides/conversations-and-end-users).
- If your client disconnects, Lookio still finishes, charges and stores the answer.
- The cost is the same as for a JSON answer.
- The idempotency key does not replay a stream. A retry is a new answer, with a new charge. Before you ask again, check [your conversations](/docs/api/conversations/list-conversations) or your usage. See [Conventions](/docs/developers/conventions#idempotency).

## Read a stream in code

```js title="Node.js"
const response = await fetch(
  "https://api.lookio.app/v1/assistants/ASSISTANT_ID/query",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LOOKIO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ question: "How long do I have to return an item?", stream: true }),
  },
);

if (!response.ok) throw new Error(await response.text()); // a JSON error, before any event

const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of response.body) {
  buffer += decoder.decode(chunk, { stream: true });
  let end;
  while ((end = buffer.indexOf("\n\n")) >= 0) {
    const block = buffer.slice(0, end);
    buffer = buffer.slice(end + 2);
    const event = /^event: (.+)$/m.exec(block)?.[1];
    const data = JSON.parse(/^data: (.+)$/m.exec(block)?.[1] ?? "null");
    if (event === "delta") process.stdout.write(data.text);
    if (event === "done") console.log("\nCost:", data.credit_cost);
    if (event === "error") throw new Error(`${data.code}: ${data.message}`);
  }
}
```

## In the CLI

In a terminal, `lookio ask ... --stream` prints the text as it comes. When stdout is not a terminal, it prints one JSON object per line: `{"event":"status","data":{...}}`. See [Commands](/docs/developers/cli/commands#asking-questions).
