> ## 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.

# Streaming WebSocket

> Real-time speech to text over a persistent WebSocket: connection options, messages, events, limits, and close codes.

The streaming WebSocket transcribes audio while you send it. You stream raw audio as binary frames, and the server sends text back as it is recognized. Use it for voice agents, live captions, phone calls, and dictation.

## Endpoint

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

For the message schemas, see the [ASR WebSocket reference](/api-reference/endpoint/asr-websocket). For complete Python and Node.js programs, see the [quickstart](/guides/speech-to-text/quickstart).

## Authenticating

Send your API key in the `chariotai-api-key` header on the WebSocket handshake. If your client cannot set handshake headers, pass it as the `api_key` query parameter instead:

```
wss://api.chariot.in/v1/asr/ws?audio_format=pcm_16000&api_key=YOUR_API_KEY
```

Prefer the header where your client allows it, and keep the key on your server. If authentication fails, the server sends an `error` event and closes with code `1008`.

## How a session works

**1. Connect**

Open the WebSocket with your options in the query string. The server replies with `session.started`. Wait for it before sending audio.

**2. Stream audio**

Send audio as binary frames as you capture it. `transcript.delta` events arrive with new text as it is recognized.

**3. Finish**

Send `input.done` after your last frame. The server transcribes the remaining audio, sends `transcript.done` with the complete transcript, and closes with code `1000`.

One connection is one session. Settings are fixed for the session. To change the language, mode, style, or audio format, open a new connection.

## Connection parameters

Pass these as query parameters on the connection URL. All are optional.

**audio\_format** `string` (default: pcm\_16000)

Encoding and sample rate of the audio you send, always mono. One of `pcm_8000`, `pcm_16000`, `pcm_22050`, `pcm_24000`, `pcm_32000`, `pcm_44100`, `pcm_48000` (signed 16-bit little-endian PCM at that rate), `ulaw_8000` (G.711 mu-law), or `alaw_8000` (G.711 A-law). Declare the rate you actually send. See [Audio formats](/guides/speech-to-text/audio-formats).

**language** `string`

Language hint for the session: `en`, `hi`, `bn`, `gu`, `kn`, `ml`, `mr`, `or`, `pa`, `sa`, `ta`, or `te`. Omit it, or pass `none`, for automatic language detection. It does not translate. See [Languages](/guides/speech-to-text/modes-and-languages#languages).

**mode** `string` (default: balanced)

`balanced` prioritizes responsiveness, for interactive applications. `high_accuracy` allows more context before committing text, which increases latency and usually gives more accurate transcripts. See [Modes](/guides/speech-to-text/modes-and-languages#modes).

**style** `string` (default: codemix)

How mixed-language speech is written. `codemix` keeps borrowed words, usually English, in their own script. `native` writes everything in the script of the spoken language. See [Styles](/guides/speech-to-text/modes-and-languages#styles).

**model\_type** `string` (default: chariot-1-asr-realtime)

The speech-to-text model. `chariot-1-asr-realtime` is the default and only value.

**idle\_timeout** `number` (default: 60)

Seconds the server waits for your next message, audio or JSON, before closing the session as idle. Must be greater than 0 and at most 300. A value outside that range rejects the connection.

**api\_key** `string`

Your API key, for clients that cannot set the `chariotai-api-key` header.

## Client to server

### Audio (binary frames)

Send audio as **binary** WebSocket frames in the declared `audio_format`. Each frame is raw samples, not a file: no WAV header, no JSON, no base64.

Any frame size works, and a frame does not have to end on a sample boundary. Frames of about 100 ms are a good default, for example 3,200 bytes at `pcm_16000`. See [chunk size](/guides/speech-to-text/audio-formats#chunk-size).

### `keepalive`

Keeps the session open while you have no audio to send, for example while a microphone is muted. It resets the idle timer. The server does not reply.

```json theme={null}
{ "type": "keepalive" }
```

### `input.done`

Signals that no more audio is coming. The server transcribes everything it has received, sends any remaining `transcript.delta` events and then `transcript.done`, and closes with `1000`. Audio sent after `input.done` is not transcribed.

```json theme={null}
{ "type": "input.done" }
```

All JSON messages are text frames. A text frame that is not valid JSON, is not a JSON object, or has an unknown `type` gets an `error` event without a `code`, and the session continues.

## Server to client

The server sends JSON text frames only.

**session.started** `event`

The session is ready for audio. Sent once, first. Echoes the settings in effect.

```json theme={null}
{
  "type": "session.started",
  "session_id": "3f2c9a71-5b0e-4d7a-9c1e-8a4b6d2f0e13",
  "model_type": "chariot-1-asr-realtime",
  "sample_rate": 16000,
  "language": "en",
  "mode": "balanced",
  "style": "codemix",
  "audio_format": "pcm_16000",
  "max_duration_seconds": 10800
}
```

| Field | Description |
| - | - |
| `session_id` | Unique id for the session. Use it to find the session in [history](/guides/speech-to-text/history). |
| `model_type` | The model serving the session. |
| `sample_rate` | Sample rate of the audio you declared, in Hz. |
| `language` | The language hint, or `null` for automatic language detection. |
| `mode` | `balanced` or `high_accuracy`. |
| `style` | `codemix` or `native`. |
| `audio_format` | The declared audio format. |
| `max_duration_seconds` | The longest this session can run. |

**transcript.delta** `event`

New text, as it is recognized. Each delta adds to the text before it, so append `text` to what you have shown so far. Spaces are included in `text`, so concatenate deltas without adding separators.

```json theme={null}
{ "type": "transcript.delta", "text": " the meeting" }
```

**transcript.done** `event`

The complete transcript, sent once after `input.done`. `audio_seconds` is the duration of the audio you sent. The connection then closes with `1000`.

```json theme={null}
{
  "type": "transcript.done",
  "text": "Hi, I'd like to move the meeting to Thursday.",
  "audio_seconds": 4.82
}
```

Treat `text` here as the final result, and use it in place of the deltas you accumulated.

**session.expiring** `event`

The session will reach `max_duration_seconds` soon. Sent once, about a minute before the limit.

```json theme={null}
{ "type": "session.expiring", "seconds_remaining": 52 }
```

**error** `event`

Something went wrong. When the event has a `code`, the error is terminal: the server closes the connection with that same close code. Without a `code`, the error concerns a single message and the session continues.

```json theme={null}
{ "type": "error", "code": 4402, "message": "Insufficient credits" }
```

## Keeping a session open

The server closes a session as idle when it receives no message for `idle_timeout` seconds (60 by default). Every audio frame and every JSON message resets the timer. An idle close sends an `error` event with code `1008` and closes with `1008`.

If you pause the audio, send `{"type": "keepalive"}` more often than the idle timeout, for example every 10 to 20 seconds. Sessions that end on an idle timeout do not get a `transcript.done`. The text recognized up to that point arrived in `transcript.delta` events and is saved in [history](/guides/speech-to-text/history).

> [!NOTE]
>
> Text for your most recent speech arrives only after more audio follows it, and `high_accuracy` holds back more of it than `balanced`. With a live source, keep streaming through pauses, silence included, so that text arrives during the pause. While you send only `keepalive`, it waits until audio resumes or you send `input.done`. When you have no more audio, send `input.done` to get the final text.

## Session length

A session can run for up to `max_duration_seconds`, announced in `session.started` (currently 10,800 seconds, three hours). About a minute before the limit, the server sends `session.expiring`. At the limit it sends an `error` event with code `4413` and closes with `4413`, without a `transcript.done`.

To transcribe a stream longer than the limit, open a new session when `session.expiring` arrives, move your audio to it, and send `input.done` on the old session to collect its final transcript.

## Concurrency

Each open session uses one speech-to-text concurrency slot for its entire life, including pauses, until the connection closes. The number of slots depends on your plan. When every slot is in use, a new session gets an `error` event and closes with `4429`. See [Rate limits & concurrency](/guides/resources/rate-limits).

Close sessions you no longer need, and send `input.done` rather than leaving a finished session open until it times out.

## Billing

Streaming costs **1.5 credits per second of audio you send**, at any sample rate or encoding. Time with the connection open but no audio sent, such as keepalive-only pauses, is not billed. Silence you send as audio is billed like any other audio.

Credits are charged periodically while the session runs and settled when it closes. A session's total is the audio duration multiplied by 1.5, rounded up to a whole credit.

To open a session, your workspace needs a small minimum balance (currently 30 credits). Otherwise the server sends an `error` event with code `4402` and closes. If your balance runs out during a session, the server sends an `error` event with code `4402` and closes, without a `transcript.done`. See [Credits & pricing](/guides/resources/credits).

## Close codes

| Code | Meaning |
| - | - |
| `1000` | Normal. The session finished after `transcript.done`. |
| `1008` | Policy: authentication failed, a query parameter is invalid, speech to text is not available on your plan, or the idle timeout fired. |
| `1011` | Server error, or a requested `language`, `mode`, or `style` cannot be served right now. For an unavailable option, the `error` message says which value to use instead. For a server error, retry after a short delay. |
| `4402` | Insufficient credits, when the session opens or while it runs. |
| `4413` | The session reached its maximum duration. |
| `4429` | Too many concurrent speech-to-text sessions for your workspace. |

The `error` event's `message` explains the cause. Branch on the code, not on the message text. When a session fails to start, nothing is billed.

## Best practices

* **Declare the right format.** Audio sent at a different rate than `audio_format` declares is transcribed as the wrong speech, without an error.
* **Wait for `session.started`** before sending audio, and read `max_duration_seconds` from it.
* **Send about 100 ms per frame** from live sources, as you capture it.
* **Always end with `input.done`** to get `transcript.done`. Closing the socket yourself skips it.
* **Use `transcript.done` as the final text.** Deltas are for live display.
* **Handle `1011`** by reading the `error` message: if it names an option to change, reconnect with that value; otherwise reconnect after a short delay. On `4429`, wait for another session to finish, or retry with backoff.
* **Set `language`** when you know it. Omit it for mixed or unknown speakers.

## Example: keepalive during pauses

This Python fragment sends audio from a queue and falls back to keepalive messages while the queue is empty, for example while a microphone is muted:

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

async def pump(ws, frames: asyncio.Queue):
    """Send audio frames from a queue. Put None in the queue to finish."""
    while True:
        try:
            frame = await asyncio.wait_for(frames.get(), timeout=15)
        except asyncio.TimeoutError:
            await ws.send(json.dumps({"type": "keepalive"}))
            continue
        if frame is None:
            await ws.send(json.dumps({"type": "input.done"}))
            return
        await ws.send(frame)  # raw 16-bit PCM bytes
```


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