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

# Stream a chat (SSE)

> Send a message and read the agent's reply live over Server-Sent Events.

HQ turns are **asynchronous**: you post a message, it runs server-side on the agent's machine, and you watch it unfold over a **Server-Sent Events (SSE)** stream. This is how you build a live chat UI or react to tool calls and artifacts as they happen.

## The flow

<Steps>
  <Step title="Create or pick a conversation">
    Create one against an agent (`agent_id` from [List agents](/api-reference/agents/list-agents)):

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/api/conversations \
      -H "Authorization: Bearer $HQ_TOKEN" -H "Content-Type: application/json" \
      -d '{"agent_id":"<AGENT_ID>"}'
    # → { "id": "<CONVERSATION_ID>", ... }
    ```
  </Step>

  <Step title="Open the event stream">
    Attach to the stream so you don't miss early events:

    ```bash theme={null}
    curl -N https://api.hq.zone/v1/api/conversations/<CONVERSATION_ID>/stream \
      -H "Authorization: Bearer $HQ_TOKEN" -H "Accept: text/event-stream"
    ```

    `-N` disables buffering so events print as they arrive.
  </Step>

  <Step title="Post a message">
    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/api/conversations/<CONVERSATION_ID>/messages \
      -H "Authorization: Bearer $HQ_TOKEN" -H "Content-Type: application/json" \
      -d '{"text":"What changed in revenue quarter over quarter?","idempotency_key":"<UUID>"}'
    # → 202 { "accepted": true, "stream_url": "/v1/api/conversations/<id>/stream", "idempotent": false }
    ```

    The response is immediate; the reply arrives on the stream you opened.
  </Step>
</Steps>

## Reading the stream

Each event has a name, an `id:` cursor, and a one-line JSON payload:

```text theme={null}
event: text_delta
id: 7Z9aQ2
data: {"text":"Revenue rose 12% "}

event: text_delta
id: 7Z9aQ3
data: {"text":"quarter over quarter."}

event: final
id: 7Z9aQ4
data: {"text":"Revenue rose 12% quarter over quarter.","duration_ms":4120}
```

Append `text_delta` chunks to the active reply; `final` (or `error`) ends the turn. The full event vocabulary and scope rules are on [Stream conversation events](/api-reference/conversations/stream).

## In JavaScript

`EventSource` can't set an `Authorization` header, so read the stream with `fetch` and a streaming body reader (works in Node 18+ and modern browsers):

```js theme={null}
const base = "https://api.hq.zone";
const headers = { Authorization: `Bearer ${process.env.HQ_TOKEN}` };
const convId = "<CONVERSATION_ID>";

// 1. Post the message (returns immediately).
await fetch(`${base}/v1/api/conversations/${convId}/messages`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ text: "What changed in revenue quarter over quarter?", idempotency_key: crypto.randomUUID() }),
});

// 2. Read the reply off the SSE stream.
const res = await fetch(`${base}/v1/api/conversations/${convId}/stream`, {
  headers: { ...headers, Accept: "text/event-stream" },
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  const frames = buf.split("\n\n");
  buf = frames.pop();                         // keep the trailing partial frame
  for (const frame of frames) {
    const ev = {};
    for (const line of frame.split("\n")) {
      if (line.startsWith("event:")) ev.event = line.slice(6).trim();
      else if (line.startsWith("data:")) ev.data = line.slice(5).trim();
      // lines starting with ":" are heartbeats — ignore
    }
    if (!ev.event) continue;
    const payload = ev.data ? JSON.parse(ev.data) : {};
    if (ev.event === "text_delta") process.stdout.write(payload.text);
    if (ev.event === "final" || ev.event === "error") return;
  }
}
```

<Tip>Track the last `id:` you saw and send it as the `Last-Event-ID` header when reconnecting — the server replays what you missed from the journal (or pass `?since_seq=<id>` on a fresh attach). Always include an `idempotency_key` in the message body so a network retry never starts a duplicate turn.</Tip>

## Tips

* **Open the stream early** — attach before or right after posting so you catch the first `text_delta`.
* **One stream, many turns** — `result`/`error` ends a *turn*, not the stream. Keep it open and post again for the next turn.
* **Stop a running turn** with [Interrupt the active turn](/api-reference/conversations/interrupt).
