> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.narilabs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.narilabs.com/_mcp/server.

# TTS input streaming

Send text and receive generated audio on the same WebSocket. Output streams automatically; there is no separate `stream: true` option.

## Connect

Open `wss://api.narilabs.com/v1/text-to-speech/{voice_id}/stream-input?model_id=qwen3-tts` with `Authorization: Bearer YOUR_API_KEY` in the handshake. Use a [voice ID](/voices) supported by the selected model. `qwen3-tts` is Standard; `qwen3-tts-fast` is Fast.

Connect from your backend: browser WebSocket clients cannot set an `Authorization` header. Keep `x-request-id` from the handshake for troubleshooting. The [WebSocket API reference](/api-reference/text-to-speech/input-stream/stream-text-to-speech) lists optional query parameters and message fields.

## Send text and flush

Send JSON text messages in this order:

```json
{"text":" "}
{"text":"Hello from "}
{"text":"Nari Labs.","flush":true}
{"text":"This is a second segment.","flush":true}
{"text":""}
```

Send each line as a separate message. Start with exactly one space; it is not billed. Later text fragments join without added spaces, so include spaces between words yourself. Audio may arrive before a flush when enough text is available; short input may wait for more text or a flush. `flush: true` completes a segment and keeps the connection open; empty text ends input. Continue receiving until the connection finishes.

Only automatic generation is supported. Omit `auto_mode` or set it to `true`; `false`, `generation_config`, `generator_config`, and `try_trigger_generation` are rejected.

## Receive audio

Receive messages concurrently while sending text:

```json
{"audio":"AAAAAA==","isFinal":false}
```

Decode `audio` from base64 into 24 kHz, 16-bit little-endian, mono PCM. There is no WAV header. Append chunks in arrival order.

The final message is:

```json
{"isFinal":true}
```

`isFinal` completes the connection. Individual flushes have no completion event; see [Request logs](/request-logs) for their results.

## Python example: two flushes

Install `websockets` version 14 or later and set `NARI_API_KEY`. This script saves both segments as one `speech.pcm` file.

```python
import asyncio
import base64
import json
import os
from pathlib import Path

from websockets.asyncio.client import connect


async def main():
    url = "wss://api.narilabs.com/v1/text-to-speech/diana/stream-input?model_id=qwen3-tts"
    headers = {"Authorization": f"Bearer {os.environ['NARI_API_KEY']}"}
    async with connect(url, additional_headers=headers) as ws:
        print("Request ID:", ws.response.headers.get("x-request-id"))
        await ws.send(json.dumps({"text": " "}))

        async def send_text():
            messages = [
                {"text": "Hello from "},
                {"text": "Nari Labs.", "flush": True},
                {"text": "This is a second segment.", "flush": True},
                {"text": ""},
            ]
            for message in messages:
                await ws.send(json.dumps(message))

        async def receive_audio():
            with Path("speech.pcm").open("wb") as output:
                async for raw in ws:
                    event = json.loads(raw)
                    if "error" in event:
                        raise RuntimeError(f"{event['error']}: {event.get('message', '')}")
                    if event.get("audio"):
                        output.write(base64.b64decode(event["audio"], validate=True))
                    if event.get("isFinal"):
                        return
            raise RuntimeError("Connection ended before isFinal; audio is incomplete")

        await asyncio.gather(send_text(), receive_audio())


if __name__ == "__main__":
    asyncio.run(main())
```

Play the PCM file with:

```bash
ffplay -nodisp -autoexit -f s16le -ar 24000 -ac 1 speech.pcm
```

If the connection ends before `isFinal`, the audio may be incomplete. See [WebSocket failures](/errors#websocket-failures) and [Usage and billing](/usage-and-billing).