Stream dialogue in real-time

This guide shows you how to stream Eleven v3 dialogue audio over the Text to Dialogue WebSocket.

The Text to Dialogue WebSocket (/v1/text-to-dialogue/stream-input) keeps a single connection open while you send dialogue lines and receive base64-encoded audio chunks. It is intended for Eleven v3 dialogue models only (model_id must start with eleven_v3).

This guide covers the Text to Dialogue WebSocket. For Flash, Multilingual v2, or other non-v3 TTS models, use the Realtime TTS WebSocket. For a side-by-side summary of both protocols, see Text to Speech vs Text to Dialogue WebSockets.

Requirements

  • An ElevenLabs account with an API key (authentication).
  • The API key must have Text to Speech permissions.
  • Python or Node.js installed on your machine.

Setup

pip install python-dotenv websockets

Create a .env file:

.env
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here

Pick a voice ID from the Voice Library. The examples below use eleven_v3_conversational, which allows one registered voice per connection.

Open the WebSocket

Connect to wss://api.elevenlabs.io/v1/text-to-dialogue/stream-input with query parameters such as model_id and output_format. You can send the API key in the xi-api-key header or in the first JSON message (shown here in the body for a single pattern across languages).

import asyncio
import base64
import json
import os
from dotenv import load_dotenv
import websockets
load_dotenv()
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
VOICE_ID = "21m00Tcm4TlvDq8ikWAM"
MODEL_ID = "eleven_v3_conversational"
URI = (
"wss://api.elevenlabs.io/v1/text-to-dialogue/stream-input"
f"?model_id={MODEL_ID}&output_format=mp3_44100_128"
)

Register voices and stream text

Send a first message that includes voices (required) and xi_api_key if you did not set the xi-api-key header. Then send one or more frames with inputs: each item has text, voice_id, and optional new_turn.

The server buffers text until it has enough context (about 40 characters and 8 words), then emits audio chunks. Response fields use snake_case (for example is_final).

async def stream_dialogue():
async with websockets.connect(URI) as websocket:
await websocket.send(
json.dumps(
{
"voices": [VOICE_ID],
"xi_api_key": ELEVENLABS_API_KEY,
}
)
)
line = (
"This is a longer line of dialogue used to exceed the minimum buffer so the model "
"starts generating streamed audio for the registered voice. "
)
await websocket.send(
json.dumps(
{
"inputs": [
{"text": line, "voice_id": VOICE_ID, "new_turn": False},
],
}
)
)
await websocket.send(json.dumps({"close_socket": True}))
os.makedirs("output", exist_ok=True)
out_path = "output/dialogue-ws.mp3"
with open(out_path, "wb") as audio_file:
while True:
raw = await websocket.recv()
msg = json.loads(raw)
if msg.get("error"):
raise RuntimeError(msg)
if msg.get("audio"):
audio_file.write(base64.b64decode(msg["audio"]))
if msg.get("is_final"):
break
print(f"Wrote {out_path}")
asyncio.run(stream_dialogue())

close_socket flushes any buffered text, sends remaining audio, then a final frame with is_final: true before the connection closes. To keep the connection open between lines, omit close_socket until the session ends; use flush to force audio for shorter buffers without closing.

Run the script

python text-to-dialogue-websocket.py

You should get an MP3 file under output/ (filename as in the example above).

Behaviour notes

Buffering

Unlike the TTS WebSocket chunk_length_schedule, dialogue streaming uses a fixed server threshold (character and word count) before the first partial audio. If you send short lines and hear delays, batch slightly more text per inputs frame or send flush: true to force generation without closing the socket.

Turns and voices

Set new_turn: true when a speaker finishes a turn so prosody resets cleanly. Changing voice_id between inputs entries also starts a new turn. With eleven_v3_conversational, register exactly one voice in voices; eleven_v3 supports up to 10 registered voices.

Inactivity

If the server receives no client message for 20 seconds, the connection ends. Send {"keep_alive": true} to reset the timer without synthesizing audio.

Concurrency

Each open connection holds one dialogue session for as long as it stays open, drawn from a dedicated pool separate from your plan’s standard concurrency limit. Audio generated over the connection does not count toward standard concurrency. See Text to Dialogue concurrency.

Alignment

Add sync_alignment=true to the query string to receive alignment objects (snake_case timing arrays) on chunks when available. See the API reference.

Next steps