Docs/API reference/WebSocket streaming

Streaming voice in real time.

Stream synthesised speech sentence by sentence over a WebSocket, so a conversational agent can start speaking the first sentence while the rest of the text is still being synthesised. This is live now for any API key.

Live in production

The streaming endpoint below is live at wss://api.celestlabs.ai/v1/stream. The non-streaming REST endpoint POST /v1/synthesize returns the whole clip in a single response; reach for the WebSocket when you want playback to begin before the full passage is rendered. You can hear streaming end to end in the live demo.

WebSocket endpoint Live

WSSwss://api.celestlabs.ai/v1/stream

Open a WebSocket and send a single JSON message carrying your API key and the text. The server synthesises the passage one sentence at a time and streams each sentence back as a binary WAV frame, then sends a final JSON summary and closes the connection. It's one request per connection, open a fresh connection for the next utterance.

The request frame

Your first (and only) message is JSON. token and text are required; everything else falls back to the defaults shown.

json
{
  "token":    "sk_live_...",                 // your API key, required
  "text":     "First sentence. Second sentence.",  // required
  "voice":    "meera",                      // optional, default "meera"
  "language": "en",                         // optional, default "en"
  "steps":    8,                            // optional, 1–64, default 8
  "speed":    1.05,                         // optional, 0–3, default 1.05
  "idempotency_key": "req-42"              // optional, echoed back as request_id
}

What the server streams back

After your request frame, the server sends a sequence of frames on the same socket:

  1. Binary frames, one per sentence-chunk, each a complete, self-contained WAV file (RIFF header followed by 16-bit PCM, mono, 44.1 kHz). Decode or play each frame as it arrives; the first lands while the later sentences are still being synthesised.
  2. A final text frame (JSON), once every sentence has been sent, summarising the request:
    json
    { "done": true, "request_id": "req-42", "chars": 33, "audio_ms": 2480 }

The server then closes the connection normally (code 1000). Because each binary frame is an independent WAV, the simplest browser pattern is to decodeAudioData each one and schedule the buffers back to back.

Audio format

Every binary frame is a self-contained WAV, 16-bit PCM, mono, 44.1 kHz, the model's native rate. There is no format negotiation on the socket. If you need μ-law / 8 kHz for telephony or Opus for WebRTC, transcode the PCM client-side (or resample once at your edge).

Close codes

Errors are signalled by the WebSocket close code, not an error frame. Read it from the close event to decide whether to retry.

CodeMeaning
1000Normal close, sent after the done frame
4401Missing or invalid API key (token)
1003Malformed first frame, or empty text
4400Unknown voice or unsupported language
4403API key not scoped for that voice or language
4402Free-tier character allowance exhausted
4429Rate limit or concurrency slot exceeded, back off and retry
1011Server-side configuration error

Full example

Open the socket, send one JSON frame, and play each WAV frame the moment it arrives. The browser needs no dependencies; the Node and Python samples use one small library each.

javascript
const ctx = new AudioContext();
let nextStart = ctx.currentTime + 0.05;

const ws = new WebSocket("wss://api.celestlabs.ai/v1/stream");
ws.binaryType = "arraybuffer";

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({
    token: "sk_live_...",
    text: "Streaming voice, sentence by sentence. Each plays as it arrives.",
    voice: "meera",
  }));
});

ws.addEventListener("message", async (evt) => {
  if(typeof evt.data === "string"){            // final { done: true, ... }
    if(JSON.parse(evt.data).done) ws.close();
    return;
  }
  // each binary frame is a complete WAV, decode and schedule it
  const buf = await ctx.decodeAudioData(evt.data);
  const src = ctx.createBufferSource();
  src.buffer = buf;
  src.connect(ctx.destination);
  src.start(nextStart);
  nextStart += buf.duration;
});
node
import WebSocket from "ws";
import fs from "node:fs";

const ws = new WebSocket("wss://api.celestlabs.ai/v1/stream");

ws.on("open", () => {
  ws.send(JSON.stringify({
    token: process.env.CELESTLABS_API_KEY,
    text: "Hello from Node, streamed sentence by sentence.",
    voice: "meera",
  }));
});

let n = 0;
ws.on("message", (data, isBinary) => {
  if(isBinary){
    fs.writeFileSync(`sentence_${n++}.wav`, data);  // each frame is a playable WAV
  } else {
    const msg = JSON.parse(data.toString());
    if(msg.done){ console.log(`done: ${msg.chars} chars, ${msg.audio_ms} ms`); ws.close(); }
  }
});
python
import asyncio, io, json, os, wave
import websockets, sounddevice as sd

async def stream():
    async with websockets.connect("wss://api.celestlabs.ai/v1/stream") as ws:
        await ws.send(json.dumps({
            "token": os.environ["CELESTLABS_API_KEY"],
            "text": "Hello from Python, streamed sentence by sentence.",
            "voice": "meera",
        }))
        out = sd.RawOutputStream(samplerate=44100, channels=1, dtype="int16")
        out.start()
        async for msg in ws:
            if isinstance(msg, bytes):              # a complete WAV per sentence
                w = wave.open(io.BytesIO(msg), "rb")
                out.write(w.readframes(w.getnframes()))
            elif json.loads(msg).get("done"):
                break
        out.stop()

asyncio.run(stream())

Throughput & streaming

The model synthesises faster than real time on a CPU, no GPU required, at a real-time factor of about 0.18, roughly 5.5× faster than playback. Because audio streams sentence by sentence, the first sentence starts playing while later sentences are still being synthesised, so perceived wait time stays short on long passages. The live demo on the home page shows the real timing end to end.

For voice agents

Pair streaming with your LLM's streaming output. As each reply (or each finished sentence) comes back from the LLM, open a stream and send it; audio for the first sentence arrives while the rest is still being synthesised, keeping the conversation moving. Open one stream per turn.

Barge-in & interruption

For natural conversational agents you need to stop speaking the moment the user starts talking. The flow is:

  1. Your VAD or ASR detects user speech.
  2. Close the WebSocket. The server stops synthesising the remaining sentences as soon as the connection drops, there's no separate cancel message to send.
  3. Stop playing audio frames already buffered on the client.
  4. Start the next turn on a fresh connection.

Billing

Streaming is metered by the number of characters synthesised, exactly like the REST endpoint and at the same per-character price. Usage for a stream is recorded once, when it completes.