Skip to main content
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:
voice_id
string
required
UUID of the voice to synthesize with.
response_format
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).
split_granularity
string
default:"sentence"
How incoming text is chunked into synthesis units. One of sentence, clause, or none.
idle_timeout
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.
enable_logging
boolean
default:"false"
When true, each generated segment is archived to your TTS history and the audio is stored for later retrieval.

Message protocol

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

Client → server

input.text
message
Append text to synthesize. The server buffers and splits it according to split_granularity.
{ "type": "input.text", "text": "Hello and welcome. " }
input.flush
message
Force the server to synthesize whatever text is buffered right now, without ending the session.
{ "type": "input.flush" }
input.done
message
Signal that no more text is coming. The server flushes the remainder and finishes the session.
{ "type": "input.done" }

Server → client

audio.start
message
A new sentence is about to stream. Carries the sentence_index, the sentence_text being spoken, the format, and sample_rate (when known).
{ "type": "audio.start", "sentence_index": 0, "sentence_text": "Hello and welcome.", "format": "pcm", "sample_rate": 44100 }
(binary frames)
audio
One or more binary frames of audio for the current sentence, in the negotiated response_format.
audio.done
message
The current sentence has finished streaming. Includes total_bytes and credits_utilized (the credits charged for this sentence).
{ "type": "audio.done", "sentence_index": 0, "total_bytes": 96044, "credits_utilized": 18 }
session.done
message
All sentences are complete. The server will close the connection normally (1000).
{ "type": "session.done", "total_sentences": 3 }
error
message
An error occurred. Carries a message and, for terminal errors, a code matching the close code.
{ "type": "error", "code": 4402, "message": "insufficient credits" }

Audio format

response_formatFrames contain
pcm (default)Raw signed 16-bit little-endian PCM, mono. Sample rate from audio.start.sample_rate (44100 Hz by default).
wavA 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.
CodeMeaning
1000Normal, the session completed successfully.
1008Policy violation, authentication failed, an invalid parameter, the voice is not ready, or the idle timeout fired.
1011Internal error, the upstream synthesis service was unavailable.
4402Insufficient 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

LimitValue
Idle timeoutConfigurable, default 60 s, maximum 300 s
Session lengthNo fixed cap on total text, send input.text as needed
Per-sentence inputGoverned by split_granularity

Example

Python
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())
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.