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

# Streaming Events

A streamed turn is a `text/event-stream`. The SDK turns each frame into a typed `TurnEvent`. The stream ends after a terminal event and `[DONE]`.

For a shopping reply you usually need three events: save the conversation id, append text as it arrives, then take the finished message (including product cards).

| Event | When | What to do |
| - | - | - |
| `turn.started` | First, once | Save `conversation_id`, `turn_id`, `message_id`. |
| `part.started` | A text or tool block begins | Add a block at `part_index`. |
| `part.delta` | Text is generating | Append `delta` to the text at `part_index`. |
| `part.completed` | A block is done | Replace the block at `part_index` with the final part. |
| `turn.completed` | Success, once | Full `message` plus `usage`. |
| `error` | The turn failed | Read `code`, `message`, `retryable`. |

`part_index` is the position in the final message. Order inside one reply is stable. See [messages and parts](/conversations/messages-and-parts.md) for what to render — `search_products` uses `output.products`; `image_search` uses `output.things`.

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
for await (const event of stream) {
  switch (event.type) {
    case "turn.started":
      conversationId = event.conversation_id;
      break;
    case "part.started":
    case "part.completed":
      parts[event.part_index] = event.part;
      break;
    case "part.delta":
      text += event.delta;
      break;
    case "turn.completed":
      done(event.message);
      break;
    case "error":
      if (event.retryable) retry();
      else showError(event.message);
      break;
  }
}
```

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for event in stream:
    if event.type == "turn.started":
        conversation_id = event.conversation_id
    elif event.type in ("part.started", "part.completed"):
        parts[event.part_index] = event.part
    elif event.type == "part.delta":
        text += event.delta
    elif event.type == "turn.completed":
        done(event.message)
    elif event.type == "error":
        if event.retryable:
            retry()
        else:
            show_error(event.message)
```

```text SSE theme={"theme":{"light":"github-light","dark":"github-dark"}}
event: turn.started
data: {"type":"turn.started","conversation_id":"conv_123","turn_id":"turn_…","message_id":"msg_…"}

event: part.delta
data: {"type":"part.delta","part_index":0,"delta":"Let me pull up some options."}

event: part.completed
data: {"type":"part.completed","part_index":1,"part":{"type":"tool","tool_name":"search_products",…}}

event: turn.completed
data: {"type":"turn.completed","message":{"parts":[ … ]},"usage":{"credits_charged":2,"searches_run":1}}
```

The stream sends a heartbeat about every 15 seconds — silence between tokens is not a dead connection. A turn that fails **before** it starts returns a normal HTTP error, not an `error` event. See [errors](/conversations/errors.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.