Realtime

One WebSocket for voice agents. Send text as your LLM writes it; speech starts after the first sentence.

POST /v1/tts needs the whole text up front. A voice agent doesn't have it: the reply arrives from the LLM a few tokens at a time. The realtime endpoint takes text as it's written and speaks each sentence as soon as it's complete, so the caller hears the first words while the rest of the reply is still being generated.

It's a plain WebSocket: connect from your server the same way a LiveKit or Pipecat agent connects to any streaming TTS. No WebRTC is involved.

Connect

GET wss://api.iodine.dev/v1/tts/realtime?voice=katie
Authorization: Bearer io_live_...
Query parameter
voicerequiredA voice id, as for POST /v1/tts.
modeloptionalv1 or v2. Left out, Iodine picks the model that has the voice.
temperatureoptionalBetween 0 and 2.
languageoptionalv2: en, es, fr, de, ja or zh, as for POST /v1/tts.

The key goes in the Authorization header, so connect from your backend; browsers can't set headers on a WebSocket, and your key must never reach a browser anyway. A bad key, an unknown voice or an empty balance is refused with a normal HTTP error before the socket opens.

Messages you send

JSON text messages:

MessageEffect
{"type": "text", "text": "Hi there, "}Add text. Complete sentences are spoken as they form.
{"type": "text", "text": "…", "flush": true}Add text and speak everything buffered now.
{"type": "flush"}Speak whatever is buffered now, even mid-sentence. Send it when the LLM's turn ends.
{"type": "cancel"}Stop speaking at once and drop buffered text. Use it when the user interrupts.
{"type": "close"}Speak what's left, send done, then close the socket.

Messages you receive

  • Binary messages: raw audio, 24 kHz mono signed 16-bit little-endian PCM. Play them in order as they arrive.
  • JSON text messages:
EventWhen
{"type": "ready", "voice": "katie", "model": "v2", "sampleRate": 24000, "encoding": "pcm_s16le", "channels": 1}Once, when the socket is ready for text.
{"type": "segment", "id": "…", "chars": 42}A piece of speech is starting. id is its request id.
{"type": "segment_done", "id": "…", "audioMs": 2310, "ms": 980}That piece is finished: audio length and time taken.
{"type": "cancelled"}Your cancel took effect.
{"type": "error", "code": "…", "message": "…"}Something went wrong, for example insufficient_credits for the next piece.
{"type": "done"}After close, once everything has been spoken.

How text is split

Text is spoken a sentence at a time. A sentence boundary only ends a piece once it holds at least 12 characters, so a lone "Hi." waits for the next sentence. Text with no sentence boundary is cut at a space once it reaches 300 characters. flush speaks whatever is buffered, however short.

Each piece is billed as one request, by the length of its audio, at the same rate as POST /v1/tts.

Example (Python)

python
import asyncio, json, os
import websockets  # pip install websockets

async def speak(chunks):
    async with websockets.connect(
        "wss://api.iodine.dev/v1/tts/realtime?voice=katie",
        additional_headers={"Authorization": f"Bearer {os.environ['IODINE_API_KEY']}"},
    ) as ws:
        print(await ws.recv())  # ready
        for chunk in chunks:    # e.g. tokens from your LLM
            await ws.send(json.dumps({"type": "text", "text": chunk}))
        await ws.send(json.dumps({"type": "close"}))
        with open("reply.pcm", "wb") as f:
            async for msg in ws:
                if isinstance(msg, bytes):
                    f.write(msg)  # play it, or forward it to your call
                elif json.loads(msg)["type"] == "done":
                    break

asyncio.run(speak(["Sure! ", "Your order shipped this morning ", "and should arrive on Thursday."]))

Limits

  • A socket with no message from you for 5 minutes is closed (idle_timeout).
  • One message can be up to 256 KB.
  • An error about one piece doesn't close the socket; keep sending, or close it yourself.