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

# Streaming

> Receive the reply token by token over server-sent events.

`POST /v1/agents/{agent_id}/chat/stream` takes exactly the same request as the synchronous chat endpoint: same body, same `Authorization` and `Idempotency-Key` headers, same `full` permission, same conversation rules, billing and error codes. Only the transport differs: the response is `text/event-stream`.

## Events

Each event is named and carries one JSON `data` line.

| Event                        | Data                                                                         | When                                                              |
| ---------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `response.created`           | `{"request_id", "conversation_id"}`                                          | As soon as the conversation is resolved                           |
| `response.output_text.delta` | `{"delta": "..."}`                                                           | For every piece of answer text as the model produces it; repeated |
| `response.completed`         | The full chat response (`request_id`, `conversation_id`, `message`, `usage`) | Always the last event of a successful stream                      |
| `error`                      | `{"code", "message", "request_id"}`                                          | Always the last event of a failed stream                          |

```text theme={null}
event: response.created
data: {"request_id":"7f20a5f0-...","conversation_id":"1c2e..."}

event: response.output_text.delta
data: {"delta":"The basic"}

event: response.output_text.delta
data: {"delta":" plan is $19 per month."}

event: response.completed
data: {"request_id":"7f20a5f0-...","conversation_id":"1c2e...","message":{"id":"66e0...","role":"assistant","content":"The basic plan is $19 per month.","created_at":"2026-09-13T12:00:00Z"},"usage":{"prompt_tokens":120,"completion_tokens":45,"total_tokens":165,"charged_usd":"0.00123400"}}
```

<Note>
  Reasoning text and tool activity are never part of the stream. `response.completed` always carries the full final text, so a client that missed some deltas can reconcile from it rather than from the concatenated deltas.
</Note>

## Errors on a stream

Failures **before** the stream is established (invalid key, validation, `idempotency_conflict`, `conversation_busy`, a disabled agent, a rate limit) are ordinary JSON [error envelopes](/guides/errors) with their HTTP status; check `response.ok` before you start reading events.

Once headers are sent the status is already `200`. A failure after that point (for example `insufficient_balance` or `internal_error`) arrives as the `error` event, with the same fields as the envelope, and closes the stream.

## Disconnects and retries

A client that drops the connection does not stop the reply: it is still produced and stored, and the idempotency record is completed. A retry with the same `Idempotency-Key` streams `response.created` followed directly by `response.completed` with the stored result; the deltas are not replayed. See [Idempotency](/guides/idempotency).

## Examples

<CodeGroup>
  ```bash curl theme={null}
  curl -N -X POST "$CHATTLER_BASE_URL/v1/agents/$CHATTLER_AGENT_ID/chat/stream" \
    -H "Authorization: Bearer $CHATTLER_API_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d '{"external_user_id": "customer-123", "message": "Tell me more"}'
  ```

  ```python Python theme={null}
  import json
  import os
  import uuid

  import httpx


  def stream_chat(external_user_id: str, message: str) -> dict:
      url = f"{os.environ['CHATTLER_BASE_URL']}/v1/agents/{os.environ['CHATTLER_AGENT_ID']}/chat/stream"
      headers = {
          "Authorization": f"Bearer {os.environ['CHATTLER_API_KEY']}",
          "Idempotency-Key": str(uuid.uuid4()),
      }
      with httpx.stream("POST", url, headers=headers, timeout=120,
                        json={"external_user_id": external_user_id, "message": message}) as response:
          if response.status_code >= 400:
              response.read()
              raise RuntimeError(response.json()["error"])
          event, completed = None, None
          for line in response.iter_lines():
              if line.startswith("event: "):
                  event = line[len("event: "):]
              elif line.startswith("data: "):
                  data = json.loads(line[len("data: "):])
                  if event == "response.output_text.delta":
                      print(data["delta"], end="", flush=True)
                  elif event == "response.completed":
                      completed = data
                  elif event == "error":
                      raise RuntimeError(f"{data['code']}: {data['message']}")
              elif line == "":
                  event = None
          return completed


  stream_chat("customer-123", "Tell me more")
  ```

  ```javascript Node theme={null}
  const baseUrl = process.env.CHATTLER_BASE_URL;
  const agentId = process.env.CHATTLER_AGENT_ID;
  const apiKey = process.env.CHATTLER_API_KEY;

  async function streamChat(externalUserId, message, onDelta) {
    const response = await fetch(`${baseUrl}/v1/agents/${agentId}/chat/stream`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ external_user_id: externalUserId, message }),
    });
    if (!response.ok) {
      const body = await response.json();
      throw new Error(`${body.error.code}: ${body.error.message}`);
    }
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let completed = null;
    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      let boundary;
      while ((boundary = buffer.indexOf("\n\n")) >= 0) {
        const frame = buffer.slice(0, boundary);
        buffer = buffer.slice(boundary + 2);
        const event = frame.match(/^event: (.+)$/m)?.[1];
        const data = JSON.parse(frame.match(/^data: (.+)$/m)?.[1] ?? "{}");
        if (event === "response.output_text.delta") onDelta(data.delta);
        else if (event === "response.completed") completed = data;
        else if (event === "error") throw new Error(`${data.code}: ${data.message}`);
      }
    }
    return completed;
  }

  const result = await streamChat("customer-123", "Tell me more", (t) => process.stdout.write(t));
  console.log("\n", result.usage);
  ```
</CodeGroup>

<Tip>
  Use `curl -N` (no buffering) to see events as they arrive. In browsers, `EventSource` cannot send `POST` bodies or custom headers, and a key must never reach a browser anyway; stream from your own server and forward the text to the client.
</Tip>
