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

# JSON quickstart

> Stream LLM tokens to Coda over a WebSocket with JSON messages in Python, and save the audio as it arrives.

Stream a short reply to Coda token by token and save the audio to `output.pcm`. You can run it without an audio device, then [convert the file to WAV](#listen-to-the-result) to hear it.

## Prepare the client

Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later. Create a [Rime API key](https://app.rime.ai/tokens), then set `RIME_API_KEY` in your server's environment. Keep the key out of browser code and source control.

Create a project and install the WebSocket client:

```bash theme={null}
uv init --python 3.11 coda-json
cd coda-json
uv add 'websockets>=15,<16'
```

## Stream audio to a file

Save this as `stream_json.py`. It connects to `wss://coda.api.rime.ai/ws` with `rime.v1.json` and overwrites `output.pcm` in the current directory. The sender and receiver run together so audio can arrive while the client sends more text. For Protobuf messages, use the [Protobuf quickstart](/api-reference/coda/websocket-v1-protobuf).

```python theme={null}
import asyncio
import base64
import json
import os

from websockets.asyncio.client import connect

URL = "wss://coda.api.rime.ai/ws"
CONTEXT_ID = "turn-1"
# Replace this list with your model's tokens, unchanged.
TOKENS = ["Your ", "order ", "shipped ", "on ", "Tuesday", ". ",
          "It ", "should ", "arrive ", "by ", "Friday", "."]


async def write_audio(output, chunk):
    task = asyncio.create_task(asyncio.to_thread(output.write, chunk))
    try:
        await asyncio.shield(task)
    except asyncio.CancelledError:
        # A worker thread cannot be cancelled. Finish the write before closing.
        while not task.done():
            try:
                await asyncio.shield(task)
            except asyncio.CancelledError:
                pass
        task.result()
        raise


async def receive_event(connection):
    raw = await connection.recv()
    if not isinstance(raw, str):
        raise RuntimeError("Expected a JSON text message")
    event = json.loads(raw)
    if "error" in event:
        raise RuntimeError(f"Rime error: {event['error']}")
    return event


async def send_tokens(connection):
    for token in TOKENS:
        await connection.send(json.dumps({"contextId": CONTEXT_ID, "text": token}))
    await connection.send(json.dumps({"contextId": CONTEXT_ID, "end": {}}))


async def receive_audio(connection, output):
    while True:
        event = await receive_event(connection)
        if event.get("contextId") != CONTEXT_ID:
            raise RuntimeError("Unexpected context ID")
        if "started" in event:
            print("Request ID:", event["started"].get("requestId"))
        elif "audio" in event:
            await write_audio(output, base64.b64decode(event["audio"], validate=True))
        elif "done" in event:
            return
        elif "cancelled" in event:
            raise RuntimeError("Synthesis was cancelled")


async def main():
    headers = {"Authorization": f"Bearer {os.environ['RIME_API_KEY']}"}
    # This example allows two minutes for the whole operation.
    async with asyncio.timeout(120):
        async with connect(
            URL, additional_headers=headers, subprotocols=["rime.v1.json"]
        ) as connection:
            if connection.subprotocol != "rime.v1.json":
                raise RuntimeError("JSON subprotocol was not selected")
            event = await receive_event(connection)
            if event.get("ready", {}).get("protocol") != 1:
                raise RuntimeError("Expected WebSocket protocol version 1")
            await connection.send(json.dumps({
                "contextId": CONTEXT_ID,
                "start": {
                    "speaker": "lyra",
                    "language": "en",
                    "text": "",
                    "splitStrategy": "SPLIT_STRATEGY_ACCUMULATE_SENTENCE",
                    "audioParameters": {
                        "audioFormat": "audio/pcm",
                        "samplingRate": 24000,
                    },
                },
            }))
            with open("output.pcm", "wb", buffering=0) as output:
                async with asyncio.TaskGroup() as tasks:
                    tasks.create_task(send_tokens(connection))
                    tasks.create_task(receive_audio(connection, output))
    print("Saved output.pcm")


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

The script has four functions:

* `send_tokens`: sends each token as a `text` message, then sends `end`. Because `start` sets `SPLIT_STRATEGY_ACCUMULATE_SENTENCE`, the server joins the tokens into sentences. To stream input over time, replace `TOKENS` with your model's output.
* `receive_audio`: reads events for `turn-1` until `done`, and writes each `audio` chunk. To route audio to a browser, phone system, or player, replace the `await write_audio(...)` call with your audio handler.
* `write_audio`: writes each chunk on a worker thread. The receiver waits for each write before it reads the next message, so chunks stay in order. On cancellation, it waits for the active write before it closes the file.
* `main`: connects, waits for `ready`, sends `start`, and runs the sender and receiver together. It closes the connection after `done`. In a persistent application, keep the socket open, start each turn with a new `contextId`, and dispatch every turn's events from one receive loop. See [lifecycle and interruption handling](/api-reference/coda/websocket-v1#run-a-synthesis-context).

Run the script after setting your API key:

```bash theme={null}
uv run stream_json.py
```

A successful run prints a request ID and `Saved output.pcm`. The file contains raw mono, signed 16-bit little-endian PCM at 24 kHz, with no WAV header, and a failed run can leave a partial file. To hear the audio, [convert it to WAV](#listen-to-the-result).

## Listen to the result

Most players do not open headerless PCM. This command wraps `output.pcm` in a WAV header and saves `output.wav`, which any audio player opens:

```bash theme={null}
uv run python - <<'PY'
import wave
from pathlib import Path

pcm = Path("output.pcm").read_bytes()
with wave.open("output.wav", "wb") as out:
    out.setnchannels(1)
    out.setsampwidth(2)
    out.setframerate(24000)
    out.writeframes(pcm)
PY
```

## Use another language or audio format

Set `language` and `speaker` together for the desired [voice](/docs/voices-coda). Set the audio MIME type in `audioParameters.audioFormat`. Every JSON `audio` payload is base64 regardless of the selected audio format. Decode it once, then send the bytes to the right player or decoder. For live playback of MP3, Opus, or WAV, use a streaming decoder for that format, and keep it open for the whole context. See [audio parameters](/api-reference/coda/websocket-v1#audio-parameters) for sample rates and byte formats.


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