> ## Documentation Index
> Fetch the complete documentation index at: https://rimelabs-docs-coda-websocket-reference.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# JSON quickstart

> Stream sentences to Coda with a standard Python WebSocket client and save audio chunks as they arrive.

This Python example sends two sentences in one streaming context and writes PCM audio chunks to a file as they arrive. It needs no audio device. It uses `rime.v1.json` on `wss://api.rime.ai/coda/ws`. For the Protobuf variant, use the [Protobuf quickstart](/api-reference/coda/websockets-binary).

## 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 overwrites `output.pcm` in the current directory. The sender and receiver run together so audio can arrive while the client sends more text.

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

from websockets.asyncio.client import connect

URL = "wss://api.rime.ai/coda/ws"
CONTEXT_ID = "turn-1"


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_sentences(connection):
    # Replace this list with complete sentences from your application's input.
    for sentence in ["Hello from Coda. ", "This audio uses the JSON protocol. "]:
        await connection.send(json.dumps({"contextId": CONTEXT_ID, "text": sentence}))
    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:
            output.write(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": "",
                    "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_sentences(connection))
                    tasks.create_task(receive_audio(connection, output))
    print("Saved output.pcm")


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

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. A failed run can leave a partial file.

To stream input over time, replace the fixed list in `send_sentences` with complete sentences from your application. To route audio to a browser, phone system, or player, replace `output.write(...)` with your audio handler.

The script closes the connection after `done`. In a persistent application, keep the socket open and start the next turn with a new `contextId`. Use one receive loop to dispatch events across turns. See [lifecycle and interruption handling](/api-reference/coda/websockets#run-a-synthesis-context).

## Run the audio demo

[Download the audio demos](/files/coda-audio-demos.zip), extract the archive, and open its `coda-audio-demos` directory. With `RIME_API_KEY` set and a local audio output device available, run:

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

This version plays PCM audio as it arrives and prints `Playback complete` after the last samples play. The archive includes the playback helper and a README with audio-device setup and troubleshooting.

## 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 appropriate player or decoder.

The audio demo accepts only raw PCM. 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/websockets#audio-parameters) for sample rates and byte formats.
