> ## 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 a widget answer to a visitor

> Ask a question in a widget thread and receive the answer as Server-Sent Events while it is written, so a visitor watches it appear rather than waiting for all.

Call this from the visitor's browser. It takes the embed's **public widget key** in `X-ARUKZ-Widget-Key`, and the browser's `Origin` must be allowed by both the deployment's `allowed_origins` and the platform-wide widget allowlist. See [Website widget](/channels/widget).

You must also send the thread's `X-ARUKZ-Visitor-Token`. Every check (key, origin, visitor token, one question at a time, input validation) runs **before** the stream opens, so a refusal is an ordinary HTTP error with a JSON body, never an event on a `200` stream. A missing token, another thread's token and an unknown conversation all return the same `404`; a question sent while another answer is being generated returns `409`.

The request body is the same as [Send a widget message](/api-reference/widget/send-thread-message). Because the request is a `POST` with custom headers, read the stream with `fetch` rather than `EventSource`. Retrying with the same `idempotency_key` after a dropped connection replays the settled answer as a stream without calling the model again.

A successful response is `200` with `Content-Type: text/event-stream`. Each event has an `id` (a sequence number that increases by one), an `event` name and a single-line JSON `data` payload. `message.created` and `answer.started` always come first; `answer.delta`, `answer.sources` and `answer.replaced` follow as the answer is written; exactly one terminal event comes last:

| Event                | `data`                                                                                                       | When                                                                                                                                                                                                                                                                                       |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `message.created`    | `conversation_id`, `user_message_id`, `answer_message_id`                                                    | First. The question is saved.                                                                                                                                                                                                                                                              |
| `answer.started`     | `answer_message_id`                                                                                          | Generation has begun.                                                                                                                                                                                                                                                                      |
| `answer.sources`     | `citations` (same shape as a message's `citations`)                                                          | Whenever the answer first cites another source, before the delta containing its `[n]` marker, and once more with the complete list just before `answer.completed`. Each carries the full list: replace, don't append. An empty list after sources were sent means the assistant abstained. |
| `answer.delta`       | `text`                                                                                                       | Each new piece of the answer. Append in order.                                                                                                                                                                                                                                             |
| `answer.replaced`    | `text`                                                                                                       | The final checks withdrew or rewrote the streamed text. Discard every earlier delta and show this text instead.                                                                                                                                                                            |
| `answer.completed`   | `answer_message_id`, `status` (`answered` or `abstained`), `abstained`, `finish_reason`, `abstention_reason` | Terminal. The answer is saved as `complete`.                                                                                                                                                                                                                                               |
| `answer.interrupted` | `answer_message_id`, `reason`                                                                                | Terminal. Generation stopped early; the partial text is kept.                                                                                                                                                                                                                              |
| `answer.error`       | `code`, `message`                                                                                            | Terminal. Generation failed after the stream opened, before any answer text was produced.                                                                                                                                                                                                  |

Exactly one terminal event ends every stream. An abstention ends with `answer.completed` and `abstained: true`; it is a successful answer, not an error. See [Streaming](/api-reference/streaming) and [How answers work](/concepts/how-answers-work).

`abstention_reason` on this surface is **coarser than the authenticated API's**, and has only two values:

| Value         | Meaning                                                                        |
| ------------- | ------------------------------------------------------------------------------ |
| `unsupported` | The sources did not support an answer.                                         |
| `unavailable` | The model returned nothing; the question was never judged against the sources. |

A widget visitor is anonymous — anyone who can load the page the widget is embedded in. Telling them apart which kind of gap they hit would let a stranger map a private knowledge base one question at a time, without ever being shown a passage. `unavailable` stays separable because it says nothing about the corpus and "try again" is different advice. It may also be `null`.

<ParamField path="conversation_id" type="string" required>
  The conversation's ID (UUID).
</ParamField>

<ParamField header="X-ARUKZ-Widget-Key" type="string" required>
  The deployment's public widget key, `arukz_wk_…`.
</ParamField>

<ParamField header="X-ARUKZ-Visitor-Token" type="string" required>
  The visitor token returned when this thread was started.
</ParamField>

<ParamField header="Origin" type="string" required>
  Set by the browser. It must exactly match an origin allowed for this deployment. When you call
  from outside a browser, set it yourself.
</ParamField>

<ParamField body="text" type="string" required>
  The question. Up to 16,000 characters, and not empty after surrounding whitespace is trimmed.
</ParamField>

<ParamField body="idempotency_key" type="string">
  Your own key for this question, up to 128 characters of `A-Z`, `a-z`, `0-9`, `_`, `.`, `:` and
  `-`.
</ParamField>

<ParamField body="metadata" type="object">
  Your own string labels for the question. At most 20 entries; keys up to 64 characters, values up
  to 512.
</ParamField>

## Response

A `text/event-stream` body of the events above. Error responses before the stream opens are JSON, in the usual `{"code": ..., "message": ...}` shape. See [Errors](/api-reference/errors).

<RequestExample>
  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // Runs on your website.
  const MITHUNAI_API = `${MITHUNAI_URL}/arukz/api/v1`
  const WIDGET_KEY = 'arukz_wk_8d0f5a2e-3c41-4b7a-9e6d-1f2a3b4c5d6e'
  const conversationId = sessionStorage.getItem('mithunai.conversation')

  const response = await fetch(
    `${MITHUNAI_API}/widget/conversations/${conversationId}/messages:stream`,
    {
      method: 'POST',
      headers: {
        'X-ARUKZ-Widget-Key': WIDGET_KEY,
        'X-ARUKZ-Visitor-Token': sessionStorage.getItem('mithunai.visitorToken'),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text: 'How do I raise my rate limit?' }),
    },
  )

  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
  let buffer = ''
  let answer = ''
  for (;;) {
    const { value, done } = await reader.read()
    if (done) break
    buffer += value
    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
      if (event === 'answer.replaced') answer = data.text
      if (event === 'answer.sources') console.log('sources', data.citations)
      if (event === 'answer.completed') console.log(answer, 'abstained:', data.abstained)
      if (event === 'answer.error') console.error(data.code, data.message)
    }
  }
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -N --request POST "$MITHUNAI_URL/arukz/api/v1/widget/conversations/0e6a9b52-7d3f-4c18-a2e5-9b8c7d6e5f4a/messages:stream" \
    --header "X-ARUKZ-Widget-Key: arukz_wk_8d0f5a2e-3c41-4b7a-9e6d-1f2a3b4c5d6e" \
    --header "X-ARUKZ-Visitor-Token: $VISITOR_TOKEN" \
    --header "Origin: https://docs.example.com" \
    --header "Content-Type: application/json" \
    --data '{"text": "How do I raise my rate limit?"}'
  ```
</RequestExample>

<ResponseExample>
  ```text 200 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  id: 1
  event: message.created
  data: {"answer_message_id":"9f8e7d6c-5b4a-4c3d-9e2f-1a0b9c8d7e6f","conversation_id":"0e6a9b52-7d3f-4c18-a2e5-9b8c7d6e5f4a","user_message_id":"4a1b2c3d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}

  id: 2
  event: answer.started
  data: {"answer_message_id":"9f8e7d6c-5b4a-4c3d-9e2f-1a0b9c8d7e6f"}

  id: 3
  event: answer.delta
  data: {"text":"Rate limits are set per plan. "}

  id: 4
  event: answer.sources
  data: {"citations":[{"anchor":"requesting-a-higher-limit","ordinal":1,"score":0.82,"snippet":"To request a higher limit, open a support ticket from the billing page.","source_id":"6e1d3c5b-9a7f-4b2e-8d0c-3f5a7b9d1e2f","title":"Rate limits","url":"https://docs.example.com/guides/rate-limits"}]}

  id: 5
  event: answer.delta
  data: {"text":"To request a higher limit, open a support ticket from the billing page [1]."}

  id: 6
  event: answer.sources
  data: {"citations":[{"anchor":"requesting-a-higher-limit","ordinal":1,"score":0.82,"snippet":"To request a higher limit, open a support ticket from the billing page.","source_id":"6e1d3c5b-9a7f-4b2e-8d0c-3f5a7b9d1e2f","title":"Rate limits","url":"https://docs.example.com/guides/rate-limits"}]}

  id: 7
  event: answer.completed
  data: {"abstained":false,"abstention_reason":null,"answer_message_id":"9f8e7d6c-5b4a-4c3d-9e2f-1a0b9c8d7e6f","finish_reason":"stop","status":"answered"}
  ```

  ```json 401 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "code": "authentication_error", "message": "Authentication is required." }
  ```

  ```json 403 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "code": "authorization_error", "message": "This widget may not be embedded from that origin." }
  ```

  ```json 404 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "code": "not_found", "message": "The requested resource was not found." }
  ```

  ```json 409 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  { "code": "conflict", "message": "This conversation is already waiting for an answer." }
  ```
</ResponseExample>
