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

# WebSocket streaming

> Real-time, incremental text-to-speech over a persistent WebSocket connection.

The WebSocket API streams speech in real time over a single persistent connection. Unlike the HTTP endpoints, you can **send text incrementally**, as it is produced by an LLM or typed by a user, and receive audio **per sentence** as it is synthesized. This is the lowest-latency option and the right choice for voice agents and long-form speech.

## Endpoint

```
wss://api.chariot.in/v1/tts/ws
```

## Authenticating

The WebSocket accepts your API key either as a header or as a query parameter (useful for browser clients that can't set headers on a WebSocket handshake):

```
wss://api.chariot.in/v1/tts/ws?voice_id=VOICE_ID&api_key=YOUR_API_KEY
```

Prefer the `chariotai-api-key` header where your client allows it. If authentication fails, the server accepts the socket, sends an `error` frame, and closes with code `1008`.

## Connection parameters

Pass these as query parameters on the connection URL:

<ParamField query="voice_id" type="string" required>
  UUID of the voice to synthesize with.
</ParamField>

<ParamField query="response_format" type="string" default="pcm">
  Audio encoding of the binary frames. One of `pcm` (raw signed 16-bit LE PCM) or `wav` (streaming WAV, a RIFF header followed by PCM).
</ParamField>

<ParamField query="split_granularity" type="string" default="sentence">
  How incoming text is chunked into synthesis units. One of `sentence`, `clause`, or `none`.
</ParamField>

<ParamField query="idle_timeout" type="number" default="60">
  Seconds the server waits for a client message before closing the session as idle. Must be greater than 0 and at most 300.
</ParamField>

<ParamField query="enable_logging" type="boolean" default="false">
  When `true`, each generated segment is archived to your TTS history and the audio is stored for later retrieval.
</ParamField>

## Message protocol

All control messages are JSON text frames. Audio is delivered as binary frames.

### Client → server

<ResponseField name="input.text" type="message">
  Append text to synthesize. The server buffers and splits it according to `split_granularity`.

  ```json theme={null}
  { "type": "input.text", "text": "Hello and welcome. " }
  ```
</ResponseField>

<ResponseField name="input.flush" type="message">
  Force the server to synthesize whatever text is buffered right now, without ending the session.

  ```json theme={null}
  { "type": "input.flush" }
  ```
</ResponseField>

<ResponseField name="input.done" type="message">
  Signal that no more text is coming. The server flushes the remainder and finishes the session.

  ```json theme={null}
  { "type": "input.done" }
  ```
</ResponseField>

### Server → client

<ResponseField name="audio.start" type="message">
  A new sentence is about to stream. Carries the `sentence_index`, the `sentence_text` being spoken, the `format`, and `sample_rate` (when known).

  ```json theme={null}
  { "type": "audio.start", "sentence_index": 0, "sentence_text": "Hello and welcome.", "format": "pcm", "sample_rate": 44100 }
  ```
</ResponseField>

<ResponseField name="(binary frames)" type="audio">
  One or more binary frames of audio for the current sentence, in the negotiated `response_format`.
</ResponseField>

<ResponseField name="audio.done" type="message">
  The current sentence has finished streaming. Includes `total_bytes` and `credits_utilized` (the credits charged for this sentence).

  ```json theme={null}
  { "type": "audio.done", "sentence_index": 0, "total_bytes": 96044, "credits_utilized": 18 }
  ```
</ResponseField>

<ResponseField name="session.done" type="message">
  All sentences are complete. The server will close the connection normally (`1000`).

  ```json theme={null}
  { "type": "session.done", "total_sentences": 3 }
  ```
</ResponseField>

<ResponseField name="error" type="message">
  An error occurred. Carries a `message` and, for terminal errors, a `code` matching the close code.

  ```json theme={null}
  { "type": "error", "code": 4402, "message": "insufficient credits" }
  ```
</ResponseField>

## Audio format

| `response_format` | Frames contain                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `pcm` (default)   | Raw signed 16-bit little-endian PCM, mono. Sample rate from `audio.start.sample_rate` (44100 Hz by default). |
| `wav`             | A streaming WAV: a RIFF header followed by the same PCM.                                                     |

## Close codes

The connection closes with one of these codes. The close reason carries a short human-readable message.

| Code   | Meaning                                                                                                           |
| ------ | ----------------------------------------------------------------------------------------------------------------- |
| `1000` | Normal, the session completed successfully.                                                                       |
| `1008` | Policy violation, authentication failed, an invalid parameter, the voice is not ready, or the idle timeout fired. |
| `1011` | Internal error, the upstream synthesis service was unavailable.                                                   |
| `4402` | Insufficient credits, the workspace ran out of credits mid-session.                                               |

## Billing

Billing on the WebSocket is **per sentence**, charged when each sentence finishes (`audio.done`), at **1 credit per character** of the synthesized sentence. The per-sentence cost is reported in the `credits_utilized` field of each `audio.done`.

If the workspace runs out of credits mid-session, the sentence currently in flight is delivered (a one-sentence grace), then the server sends an `error` frame with code `4402` and closes. You are not charged for the ungrantable sentence.

## Limits

| Limit              | Value                                                   |
| ------------------ | ------------------------------------------------------- |
| Idle timeout       | Configurable, default 60 s, maximum 300 s               |
| Session length     | No fixed cap on total text, send `input.text` as needed |
| Per-sentence input | Governed by `split_granularity`                         |

## Example

```python Python theme={null}
import json
import websockets
import asyncio

async def main():
    url = (
        "wss://api.chariot.in/v1/tts/ws"
        "?voice_id=bac7d666-094d-4698-91fa-741d60fce662"
    )
    headers = {"chariotai-api-key": "YOUR_API_KEY"}

    async with websockets.connect(url, additional_headers=headers) as ws:
        await ws.send(json.dumps({"type": "input.text", "text": "Hello and welcome. "}))
        await ws.send(json.dumps({"type": "input.text", "text": "This is Chariot streaming over a WebSocket."}))
        await ws.send(json.dumps({"type": "input.done"}))

        async for message in ws:
            if isinstance(message, bytes):
                play_pcm(message)  # 44.1 kHz, mono, s16le
            else:
                event = json.loads(message)
                if event["type"] == "audio.done":
                    print("charged", event["credits_utilized"], "credits")
                elif event["type"] == "session.done":
                    break
                elif event["type"] == "error":
                    raise RuntimeError(event["message"])

asyncio.run(main())
```

<Note>
  Send `input.text` as soon as each fragment of text is available, you don't need to wait for a full sentence. The server assembles sentences and streams audio for each as it becomes complete.
</Note>
