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

# Multi-Context Streaming

> WebSocket endpoint: /ws/tts/multi — up to 20 independent audio streams over one connection.

Manage up to 20 independent audio streams over a single WebSocket connection.
Useful for multi-speaker conversations, pre-buffering, and interleaved audio.
The conceptual guide is [Multi-context streaming](/streaming/multi-context).

<ParamField path="WebSocket" method="/ws/tts/multi" />

## Connection

```
wss://api.kugelaudio.com/ws/tts/multi?api_key=YOUR_API_KEY
```

## Client → Server Messages

| Message                                                                     | Description                                                                                                                                                         |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{"text": " ", "context_id": "ctx1", "voice_settings": {"voice_id": 1071}}` | Initialize context with voice                                                                                                                                       |
| `{"text": "Hello", "context_id": "ctx1"}`                                   | Send text to context                                                                                                                                                |
| `{"text": "...", "context_id": "ctx1", "flush": true}`                      | Send text and flush buffer                                                                                                                                          |
| `{"flush": true, "context_id": "ctx1"}`                                     | Flush context buffer                                                                                                                                                |
| `{"text": "", "context_id": "ctx1"}`                                        | **Keep-alive**: an empty-text frame resets the context's inactivity timeout without generating audio                                                                |
| `{"close_context": true, "context_id": "ctx1"}`                             | Close a context, letting queued sentences finish first                                                                                                              |
| `{"close_context": true, "context_id": "ctx1", "immediate": true}`          | **Barge-in**: cancel the context's in-flight generation immediately and drop buffered text — see [Barge-in](/streaming/barge-in#barge-in-on-multi-context-sessions) |
| `{"update_settings": {"cfg_scale": 1.5, ...}}`                              | Change session generation parameters mid-connection (no `context_id`) — see [Updating settings](#updating-settings)                                                 |
| `{"close_socket": true}`                                                    | Close all contexts and connection                                                                                                                                   |

## Server → Client Messages

| Message                                                                                                                                         | Description                                                                                                                                                                                                                                                 |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{"context_created": true, "context_id": "ctx1"}`                                                                                               | Context created                                                                                                                                                                                                                                             |
| `{"generation_started": true, "context_id": "ctx1", "chunk_id": 0, "text": "..."}`                                                              | Generation started                                                                                                                                                                                                                                          |
| `{"audio": "base64...", "enc": "pcm_s16le", "context_id": "ctx1", "idx": 0, "sr": 24000, "samples": 4800, "chunk_id": 0}`                       | Audio chunk ([field reference](/api-reference/tts/audio-formats#audio-chunk-fields))                                                                                                                                                                        |
| `{"chunk_complete": true, "context_id": "ctx1", "chunk_id": 0, "audio_seconds": 1.2, "gen_ms": 150}`                                            | Chunk complete                                                                                                                                                                                                                                              |
| `{"word_timestamps": [...], "context_id": "ctx1", "chunk_id": 0}`                                                                               | Word-level time alignments (when enabled)                                                                                                                                                                                                                   |
| `{"settings_updated": true, "settings": {...}}`                                                                                                 | Acknowledges an `update_settings` message; `settings` holds the generation parameters now in effect — see [Updating settings](#updating-settings)                                                                                                           |
| `{"final": true, "context_id": "ctx1"}`                                                                                                         | **End of audio for a flush** (ElevenLabs `is_final` equivalent): every audio frame for text sent before your `{"flush": true}` has been delivered. Also sent right before `context_closed` on a graceful close. Not sent on an `immediate` (barge-in) close |
| `{"context_closed": true, "context_id": "ctx1", "usage": {"audio_seconds": 4.1, "cost_cents": 0.37, "currency": "eur", "model_id": "kugel-3"}}` | Context closed (terminal — all audio sent). `usage` carries this conversation's audio time + amount charged (EUR cents; `null` + `cost_unavailable` if undetermined)                                                                                        |
| `{"session_closed": true, "total_audio_seconds": 5.4}`                                                                                          | Session ended (all contexts). Per-conversation usage is on each `context_closed`, not here                                                                                                                                                                  |

The optional `usage` object on `context_closed` is present for organization-
backed requests:

| Field              | Type           | Description                                                         |
| ------------------ | -------------- | ------------------------------------------------------------------- |
| `audio_seconds`    | number         | Audio generated for this context.                                   |
| `characters`       | integer        | Input characters submitted; omitted when none were recorded.        |
| `cost_cents`       | number \| null | Actual charge in EUR cents; `null` when it could not be determined. |
| `currency`         | string         | Currency of `cost_cents`; present only when a charge is available.  |
| `cost_unavailable` | boolean        | `true` when the charge could not be determined; otherwise absent.   |
| `model_id`         | string         | Model billing identifier used for the context.                      |

## Voice Settings

When creating a context, pass voice settings as a nested object:

```json theme={null}
{
  "voice_settings": {
    "voice_id": 1071,
    "cfg_scale": 2.0,
    "max_new_tokens": 2048
  }
}
```

| Field            | Type              | Default                          | Description                                                                                                                               |
| ---------------- | ----------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `voice_id`       | integer \| string | unset                            | Voice handle or legacy numeric ID. An empty message can create a context without one, but it is required before the first non-empty text. |
| `cfg_scale`      | number            | session value (`2.0` initially)  | Per-context override, clamped to `1.2`–`2.5`.                                                                                             |
| `max_new_tokens` | integer           | session value (`2048` initially) | Per-context override, 1–2048.                                                                                                             |
| `sample_rate`    | integer           | session value                    | Per-context format input; options: 8000, 16000, 22050, 24000, 44100.                                                                      |
| `output_format`  | string            | session value                    | Per-context combined codec + rate token.                                                                                                  |

## Session-Level Config

These options can be set on any message and apply to the entire session:

| Parameter           | Type        | Default   | Description                                                                                                                                                                                                         |
| ------------------- | ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model_id`          | string      | `kugel-3` | Model to use for generation. Use `kugel-3` for new integrations.                                                                                                                                                    |
| `cfg_scale`         | number      | `2.0`     | Session classifier-free guidance value, clamped to `1.2`–`2.5`; a context's `voice_settings` can override it.                                                                                                       |
| `temperature`       | number      | unset     | Sampling variance, 0.0–1.0. Omission leaves the engine setting unset.                                                                                                                                               |
| `max_new_tokens`    | integer     | `2048`    | Session maximum tokens per generation, 1–2048; a context's `voice_settings` can override it.                                                                                                                        |
| `sample_rate`       | integer     | `24000`   | Output sample rate in Hz. Options: 8000, 16000, 22050, 24000, 44100.                                                                                                                                                |
| `output_format`     | string      | omitted   | Combined codec + rate token (for example `ulaw_8000`) — see [Audio formats](/api-reference/tts/audio-formats). May be sent top-level or inside `voice_settings`.                                                    |
| `normalize`         | boolean     | `true`    | Enable text normalization.                                                                                                                                                                                          |
| `language`          | string      | unset     | Supported language code for normalization.                                                                                                                                                                          |
| `word_timestamps`   | boolean     | `false`   | Enable word-level timestamp alignment.                                                                                                                                                                              |
| `project_id`        | integer     | omitted   | Project whose dictionaries should be loaded; required with non-empty `dictionary_ids`.                                                                                                                              |
| `dictionary_ids`    | `integer[]` | omitted   | Per-session [dictionary](/features/dictionaries) selection. `[]` = none; a non-empty list = exactly those project dictionaries (including inactive ones), bypassing the language filter, and requires `project_id`. |
| `speed`             | number      | `1.0`     | Playback speed multiplier, 0.8–1.2.                                                                                                                                                                                 |
| `flush_timeout_ms`  | integer     | `500`     | Flush buffered complete text after this many milliseconds of inactivity.                                                                                                                                            |
| `max_buffer_length` | integer     | `10000`   | Maximum buffered characters before a forced flush.                                                                                                                                                                  |

Reuse the same `context_id` across turns to keep one context alive
(recommended for a single conversation), or open new ids for parallel
speakers:

```json theme={null}
// Create / address a context. Session-level fields (sample_rate,
// output_format, language, …) may be sent top-level or inside voice_settings.
{
  "context_id": "call-42",
  "text": "Hello, how can I help you today?",
  "output_format": "ulaw_8000",
  "voice_settings": { "voice_id": 1071, "cfg_scale": 2.0 }
}
```

## Updating Settings

Change the session's generation parameters mid-connection with an
`update_settings` message (no `context_id` — it is session-scoped). The server
replies with `settings_updated`:

```json theme={null}
{
  "update_settings": {
    "cfg_scale": 1.5,
    "temperature": 0.3,
    "speed": 1.1,
    "max_new_tokens": 2048,
    "language": "de",
    "normalize": true
  }
}
```

Only these generation parameters are updatable; every field is optional.
Identity, project, dictionary, and audio-format fields (`voice_id`, `model_id`,
`sample_rate`, `output_format`, `project_id`, `dictionary_ids`) are rejected
inside `update_settings` with a `VALIDATION_ERROR` frame. Ordinary context
messages use the session-level fields in the table above.

<Note>
  **Applies to contexts started after the update.** A context's generation
  parameters are bound when its backend session opens, so an
  already-streaming context keeps its settings; the update affects contexts
  created after it. With the common one-context-per-turn pattern that means it
  takes effect on the next turn. (Per-context `cfg_scale` / `max_new_tokens` set
  in a context's `voice_settings` still win for that context.)
</Note>

## Example

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import websockets
  import json
  import base64

  async def multi_speaker():
      uri = "wss://api.kugelaudio.com/ws/tts/multi?api_key=YOUR_API_KEY"

      async with websockets.connect(uri) as ws:
          # Create narrator context
          await ws.send(json.dumps({
              "text": " ",
              "context_id": "narrator",
              "voice_settings": {"voice_id": 1071},
          }))

          # Create character context
          await ws.send(json.dumps({
              "text": " ",
              "context_id": "character",
              "voice_settings": {"voice_id": 1072},
          }))

          # Send text to different speakers
          await ws.send(json.dumps({
              "text": "The story begins.",
              "context_id": "narrator",
              "flush": True,
          }))

          await ws.send(json.dumps({
              "text": "Hello, I'm the main character!",
              "context_id": "character",
              "flush": True,
          }))

          # Receive audio from both contexts. After both flushed turns reach
          # their final frame, close the connection gracefully.
          completed = set()
          async for message in ws:
              data = json.loads(message)

              if "audio" in data:
                  ctx = data["context_id"]
                  audio_bytes = base64.b64decode(data["audio"])
                  print(f"[{ctx}] Chunk {data['idx']}: {len(audio_bytes)} bytes")

              if data.get("context_closed"):
                  usage = data.get("usage", {})
                  # Per-context (per-conversation) usage: audio time + charge (EUR cents)
                  print(f"[{data['context_id']}] usage: {usage.get('audio_seconds')}s, "
                        f"{usage.get('cost_cents')} ct")

              if data.get("final"):
                  completed.add(data["context_id"])
                  if completed == {"narrator", "character"}:
                      await ws.send(json.dumps({"close_socket": True}))

              if data.get("session_closed"):
                  break

  asyncio.run(multi_speaker())
  ```

  ```javascript JavaScript theme={null}
  const API_KEY = 'YOUR_API_KEY';
  const WS_URL = 'wss://api.kugelaudio.com';

  const ws = new WebSocket(`${WS_URL}/ws/tts/multi?api_key=${API_KEY}`);
  const audioQueues = new Map();
  const completed = new Set();

  ws.onopen = () => {
    // Create narrator context
    ws.send(JSON.stringify({
      text: ' ',
      context_id: 'narrator',
      voice_settings: { voice_id: 1071 },
    }));

    // Create character context
    ws.send(JSON.stringify({
      text: ' ',
      context_id: 'character',
      voice_settings: { voice_id: 1072 },
    }));

    // Send text to different speakers
    ws.send(JSON.stringify({
      text: 'The story begins.',
      context_id: 'narrator',
      flush: true,
    }));

    ws.send(JSON.stringify({
      text: 'Hello, I\'m the main character!',
      context_id: 'character',
      flush: true,
    }));
  };

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);

    if (data.audio) {
      const ctx = data.context_id;
      const binary = atob(data.audio);
      const bytes = new Uint8Array(binary.length);
      for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);

      if (!audioQueues.has(ctx)) audioQueues.set(ctx, []);
      audioQueues.get(ctx).push(bytes);
      console.log(`[${ctx}] Chunk ${data.idx}: ${bytes.length} bytes`);
    }

    if (data.context_closed) {
      // Per-context (per-conversation) usage: audio time + actual charge
      console.log(`[${data.context_id}] usage:`, data.usage);
    }

    if (data.final) {
      completed.add(data.context_id);
      if (completed.size === 2) ws.send(JSON.stringify({ close_socket: true }));
    }

    if (data.session_closed) {
      ws.close();
    }
  };
  ```

  ```bash cURL (wscat) theme={null}
  # Install wscat: npm install -g wscat
  wscat -c "wss://api.kugelaudio.com/ws/tts/multi?api_key=YOUR_API_KEY"

  # Create narrator context
  > {"text": " ", "context_id": "narrator", "voice_settings": {"voice_id": 1071}}

  # Create character context
  > {"text": " ", "context_id": "character", "voice_settings": {"voice_id": 1072}}

  # Send text to narrator
  > {"text": "The story begins.", "context_id": "narrator", "flush": true}

  # Send text to character
  > {"text": "Hello, I'm the main character!", "context_id": "character", "flush": true}

  # Close all contexts
  > {"close_socket": true}
  ```
</CodeGroup>

## Limits

* Maximum **20 concurrent contexts** per connection
* Contexts auto-close after **20 seconds** of inactivity (send the empty-text
  keep-alive to reset)
* Opening a context beyond the limit returns a per-context error (`error_code: "TOO_MANY_CONTEXTS"`, `code: 429`) without closing the connection — close an existing context, or wait for an idle one to be released, then retry.

## Errors

See [Error Codes](/api-reference/errors) for the full TTS error lookup table,
including HTTP status codes, WebSocket close codes, and rate-limit behavior.
