> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mithunai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream answers with server-sent events

> Receive an answer as it is written, over a text/event-stream response. The event types and their order, how citations arrive, and how to end a stream early.

Stream an answer when a person is waiting for it. Add `:stream` to the message endpoint:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
POST /conversations/{conversation_id}/messages:stream
```

The request body is the same as [Send a message](/api-reference/conversations/send-message). The response is `text/event-stream`. For scripts and batch jobs, the non-streaming endpoint returns the same answer in one response.

## Events

Each event has an `id` (its sequence number), an `event` type and a single-line JSON `data` payload:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
id: 3
event: answer.delta
data: {"text":"Mint a new key, "}

```

| Event                | Terminal | Data                                                                                                         |
| -------------------- | :------: | ------------------------------------------------------------------------------------------------------------ |
| `message.created`    |          | `conversation_id`, `user_message_id`, `answer_message_id`                                                    |
| `answer.started`     |          | `answer_message_id`                                                                                          |
| `answer.delta`       |          | `text`: the next piece of the answer                                                                         |
| `answer.sources`     |          | `citations`: the full citation list so far                                                                   |
| `answer.replaced`    |          | `text`: the whole answer, replacing every earlier delta                                                      |
| `answer.completed`   |    Yes   | `answer_message_id`, `status` (`answered` or `abstained`), `abstained`, `finish_reason`, `abstention_reason` |
| `answer.interrupted` |    Yes   | `answer_message_id`, `reason`                                                                                |
| `answer.error`       |    Yes   | `code`, `message`                                                                                            |

Field-level detail is on [Stream a message](/api-reference/conversations/stream-message).

## Rules your client can rely on

1. **Exactly one terminal event** ends every stream: `answer.completed`, `answer.interrupted` or `answer.error`.
2. **Append `answer.delta` text** in order. Each delta is an increment, not the answer so far.
3. **On `answer.replaced`, discard what you rendered** and show its `text` instead. It is sent when the final checks withdraw or rewrite streamed text, for example when an answer becomes an abstention. Later deltas append to it.
4. **Replace, don't append, citations.** `answer.sources` is sent each time the answer first cites another source, before the delta that contains its `[n]` marker, so markers can be linked as soon as they appear. When the answer has citations, one more `answer.sources` with the complete list arrives just before `answer.completed`. The last one is authoritative; an empty list means the assistant abstained.
5. **The `event:` line is the type.** It is not repeated inside `data`.
6. **`id` increases by one.** A gap means an event was lost.
7. **Ignore event types you don't recognise.** New non-terminal events may be added.

## A 200 is not success

Authentication, validation and the one-question-at-a-time check all happen before the stream opens, so those failures arrive as ordinary HTTP errors. Once the stream has opened with `200`, a failure arrives as `answer.error`. **Always read the terminal event** before treating an answer as complete.

An abstention streams like any answer: its sentence arrives as `answer.delta` text, or as `answer.replaced` when it withdraws text that was already streamed, and `answer.completed` carries `abstained: true`. Render it as a normal reply.

`answer.completed` also carries `abstention_reason` — **why** the assistant declined, or `null`. Use it to tell "nothing in your sources covers this" apart from "something does, but not confidently enough": those call for different actions. Treat it as an open string — match the values you know, fall back to a generic notice for one you do not. The values, and the shorter set the widget receives, are on [Stream a message](/api-reference/conversations/stream-message).

`answer.interrupted` means generation stopped part-way after producing some text, and that partial text is saved. Its `reason` says why: an error code such as `provider_failure` when generation failed after text had streamed, or `incomplete_stream` when generation ended without finishing. Show the partial answer with a notice that it is incomplete. A failure before any text arrives is `answer.error` instead.

## Disconnects and retries

If the client disconnects, the answer is saved with the text produced so far, marked interrupted, and the conversation is freed. Streams cannot be resumed.

If you retry with the same `idempotency_key` after a turn has finished, the stored turn is replayed as a stream and the model is not asked again. Read an answer back at any time with [List messages](/api-reference/conversations/list-messages).

## Proxies

Streaming requires every proxy between your client and MITHUNAI to pass events through unbuffered. Responses carry `X-Accel-Buffering: no`; make sure your proxies honour it and do not buffer `text/event-stream`. A buffering proxy makes the answer appear all at once at the end.

## Example

<CodeGroup>
  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch(
    `${process.env.MITHUNAI_URL}/arukz/api/v1/conversations/${conversationId}/messages:stream`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.MITHUNAI_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text: question, idempotency_key: crypto.randomUUID() }),
    },
  )
  if (!response.ok) throw new Error(JSON.stringify(await response.json()))

  let answer = ''
  let citations = []
  let buffer = ''
  const decoder = new TextDecoder()

  for await (const chunk of response.body) {
    buffer += decoder.decode(chunk, { stream: true })
    let boundary
    while ((boundary = buffer.indexOf('\n\n')) !== -1) {
      const record = buffer.slice(0, boundary)
      buffer = buffer.slice(boundary + 2)
      const event = record.match(/^event: (.*)$/m)?.[1]
      const data = JSON.parse(record.match(/^data: (.*)$/m)?.[1] ?? '{}')

      if (event === 'answer.delta') answer += data.text
      else if (event === 'answer.replaced') answer = data.text
      else if (event === 'answer.sources') citations = data.citations
      else if (event === 'answer.completed')
        console.log({ answer, citations, abstained: data.abstained })
      else if (event === 'answer.interrupted') console.warn('Partial answer:', answer)
      else if (event === 'answer.error') console.error(data.code, data.message)
    }
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import json
  import os
  import uuid
  import requests

  url = f"{os.environ['MITHUNAI_URL']}/arukz/api/v1/conversations/{conversation_id}/messages:stream"
  body = {"text": question, "idempotency_key": str(uuid.uuid4())}

  answer, citations, event = "", [], None
  with requests.post(
      url,
      headers={"Authorization": f"Bearer {os.environ['MITHUNAI_API_KEY']}"},
      json=body,
      stream=True,
      timeout=120,
  ) as response:
      response.raise_for_status()
      for line in response.iter_lines(decode_unicode=True):
          if line.startswith("event: "):
              event = line[7:]
          elif line.startswith("data: "):
              data = json.loads(line[6:])
              if event == "answer.delta":
                  answer += data["text"]
              elif event == "answer.replaced":
                  answer = data["text"]
              elif event == "answer.sources":
                  citations = data["citations"]
              elif event == "answer.completed":
                  print(answer, citations, data["abstained"])
              elif event == "answer.error":
                  raise RuntimeError(f"{data['code']}: {data['message']}")
  ```
</CodeGroup>
