# Telnyx Voice: TTS — Full Documentation > Complete page content for TTS (Voice section) of the Telnyx developer docs (https://developers.telnyx.com). > This file: https://developers.telnyx.com/docs/development/llms/voice-tts-llms-full-txt · Root index: https://developers.telnyx.com/llms.txt ## ### Overview > Source: https://developers.telnyx.com/docs/voice/tts/overview.md ## 1. Choose your interface Real-time streaming. Send text, receive audio chunks as they're synthesized. HTTP POST. Get audio back as binary, base64, or async URL. OpenAI SDK compatible. TTS during live calls via Call Control `speak` or TeXML ``. ## 2. Choose a pre-built voice Browse and filter voices by provider, model, and language. Natural, NaturalHD, Ultra, Kokoro, Qwen3TTS, xAI Grok. AWS Polly, Azure, ElevenLabs, Minimax, MurfAI, Rime, Resemble, Inworld, Fish Audio. ## 3. Or create your own Clone and design custom voices. Available on select providers: Qwen3TTS, Ultra, Minimax. --- ### Text-to-Speech Available Voices > Source: https://developers.telnyx.com/docs/voice/tts/available-voices.md Telnyx gives you access to a wide range of voices through one API. Choose from multiple providers and tiers to balance quality, tone, and cost for every interaction, giving you added flexibility to match each use case perfectly. ### Telnyx voices | Voice | Description | |-------|-------------| | **Kokoro TTS / Telnyx Natural** | Reliable and budget-friendly. Best for high-volume prompts, IVR menus, and day-to-day status updates. | | **Telnyx NaturalHD** | Great balance of quality and value. Crisp delivery, refined prosody, and disfluency handling (like "um" and "uh"). | ### Other voices Telnyx also supports the following third-party TTS providers: | Provider | Description | |----------|-------------| | **AWS Neural** | Amazon Polly neural voices. | | **Azure Neural** | Microsoft Azure neural text-to-speech voices. | | **Azure Neural HD** | High-definition Azure voices with enhanced clarity. | | **ElevenLabs** | Premium AI voices with expressive capabilities. | | **MiniMax** | High-quality multilingual voices with expressive tones. | | **ResembleAI** | Built on the Chatterbox model, delivering AI voices that preserve emotion, style, and accent for natural sounding delivery. | | **Inworld** | Expressive multilingual AI voices with Mini, Max, and TTS2 models. Use `Inworld..` format. | For configuration details on third-party providers, see the [Text-to-Speech guide](/docs/voice/programmable-voice/tts). Use the interactive explorer below to browse and filter voices by provider, model, and language. Interactive voice explorer (web only). Retrieve the full machine-readable list of available voices with `GET https://api.telnyx.com/v2/text-to-speech/voices`. --- ## WebSocket Streaming ### Lifecycle > Source: https://developers.telnyx.com/docs/voice/tts/websocket-streaming.md Real-time text-to-speech over a persistent WebSocket connection. Send text, receive audio. ## Endpoint ``` wss://api.telnyx.com/v2/text-to-speech/speech ``` ## Connection Lifecycle ### 1. Handshake There are two ways to establish a connection: #### Direct WebSocket connection You can connect directly to the WebSocket endpoint by passing all configuration as query parameters in the `wss://` URL: ``` wss://api.telnyx.com/v2/text-to-speech/speech?voice=Telnyx.NaturalHD.astra ``` Most WebSocket clients and libraries support this natively — simply open a WebSocket connection to the URL and begin the message flow. No separate HTTP request is needed. #### HTTP upgrade Alternatively, initiate the connection as an HTTP GET request that upgrades to a WebSocket via the standard `101 Switching Protocols` handshake. This is what happens under the hood when a WebSocket client connects, and may be relevant if you need fine-grained control over the upgrade (e.g., setting custom headers in environments where the WebSocket library doesn't expose them directly). #### Initialization frame Regardless of how the connection is established, send an initialization frame before any text: ```json {"text": " "} ``` The initialization frame may include `voice_settings` to configure provider-specific parameters: ```json { "text": " ", "voice_settings": { "voice_speed": 1.2 } } ``` All configuration — query parameters and voice settings — is locked before synthesis begins. See [Configuration](/docs/voice/tts/websocket-streaming/configuration) for both surfaces and the full parameter reference. ### 2. Streaming Once initialized, text and audio flow concurrently — no request/response pairing. Text is buffered and synthesized at sentence boundaries. **Client → Server** | Frame type | Content | |-----------|---------| | text | `{"text": "Hello."}` — text to synthesize | | text | `{"text": "...", "flush": true}` — force immediate synthesis of buffered text | | text | `{"force": true}` — interrupt current synthesis (barge-in), restart worker | | text | `{"text": ""}` — end of sequence, flush remaining buffer and close | **Server → Client** | Message | Description | |---------|-------------| | Audio chunk | `{"audio":"","text":"Hello.","isFinal":false}` | | Streamed chunk | `{"audio":"","text":null,"isFinal":false}` (most providers) | | Final frame | `{"audio":null,"text":"","isFinal":true}` — synthesis complete | | Error | `{"error":"..."}` — connection closes after | See [Messages](/docs/voice/tts/websocket-streaming/messages) for the complete wire protocol reference. ``` Client → Server {"text":" "} (handshake) Client → Server {"text":"Hello, welcome."} Client → Server {"text":" How are you?"} (sentence boundary detected) Client ← Server {"audio":"","isFinal":false} (streamed chunks) Client ← Server {"audio":"","isFinal":false} Client ← Server {"audio":null,"isFinal":true} (synthesis complete) Client → Server {"text":""} (end of sequence) Client ← Server remaining audio + final frame connection closes ``` **Text buffering:** Text accumulates until the server detects a sentence boundary (period, question mark, exclamation). Short fragments without punctuation wait for more text. Send `"flush": true` to force synthesis of buffered partials. **Text preprocessing:** Markdown formatting is automatically stripped before synthesis (headers, bold, italics, code blocks, links, lists, emoji). Useful when synthesizing LLM output. Pronunciation dictionary replacements are applied if `pronunciation_dict_id` is set. **Streamed vs. concatenated delivery:** Most providers (Telnyx Natural/NaturalHD/Qwen3TTS, Rime, Minimax, Resemble, Inworld) stream audio in separate frames where `text` is `null`. AWS Polly and Azure return audio in the text-bearing chunk instead. See [Messages](/docs/voice/tts/websocket-streaming/messages) for details. ### 3. Teardown Send `{"text": ""}` (empty string) to flush remaining buffered text and close gracefully. The server finishes synthesis, sends any remaining audio and a final frame, then closes the WebSocket. ``` Client → Server {"text":""} Client ← Server final audio chunks Client ← Server {"audio":null,"text":"","isFinal":true} Client ← Server [connection closed] ``` Dropping the connection without the empty-text frame works but may lose buffered text. The connection also closes on server error or inactivity timeout. See [Examples](/docs/voice/tts/websocket-streaming/examples) for complete code samples. --- ### Configuration > Source: https://developers.telnyx.com/docs/voice/tts/websocket-streaming/configuration.md ## Two Configuration Surfaces | Surface | When | What | Mutable? | |---------|------|------|----------| | Query parameters | WebSocket URL | Voice selection, audio format, sample rate, connection options | No — locked at connect | | `voice_settings` | Init frame (`{"text": " "}`) | Provider-specific tuning (speed, pitch, format, etc.) | No — locked at init | Both are one-shot. After the init frame, no configuration can change for the session. To change settings, open a new connection. ## Query Parameters Set on the URL at connect time. Immutable for the session. ### Voice Selection | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `voice` | string | — | Voice identifier in `Provider.Model.VoiceId` format. | The `voice_id` segment (third part of the `voice` string) refers to different things depending on the provider and model: | Type | Example | How you get it | |------|---------|----------------| | **Pre-built voice** | `Telnyx.NaturalHD.astra` | Browse via the [Voices API](https://developers.telnyx.com/api-reference/text-to-speech-commands/list-available-voices) or [Voice Design](https://portal.telnyx.com/#/app/ai/voice-design-lab). Shipped by the provider — available to everyone. | | **Your cloned voice** | `Telnyx.Qwen3TTS.my-ceo-clone` | Create in the [Voice Design](https://portal.telnyx.com/#/app/ai/voice-design-lab). Scoped to your organization — only your API key can use it. Available for Qwen3TTS and Minimax. | | **BYOK provider voice** | `elevenlabs.v3.Adam` | A voice ID from your own ElevenLabs or Resemble account. You bring your own API key; Telnyx relays the request. | The Voices API (`GET /v2/ai/tts/voices`) returns all voices available to your account — pre-built and cloned — with each voice's compound `id` ready to use as the `voice` parameter. ### Connection Options | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `language` | string | — | BCP-47 language code. Passed to the provider as `language_code`. Only used by providers that accept it (AWS Polly, Azure, ElevenLabs, Inworld). | | `text_type` | string | `text` | Text type hint: `text` or `ssml`. Only AWS Polly and Azure use this. | | `audio_format` | string | `mp3` | Output audio format: `mp3`, `linear16`, `wav`, `mulaw`, `alaw`, `ogg_vorbis`. Not all formats are supported by every provider — see providers dedicated pages. | | `sample_rate` | integer | provider default | Output sample rate in Hz. Accepted values vary by provider — see providers dedicated pages. | | `disable_cache` | boolean | `false` | Bypass the audio cache and always synthesize fresh. | ### Example ``` wss://api.telnyx.com/v2/text-to-speech/speech?voice=Telnyx.NaturalHD.astra&audio_format=linear16&disable_cache=true ``` ## Voice Settings Provider-specific tuning (speed, pitch, format, emotion, etc.) is not set via query parameters. It is passed once in the `voice_settings` object on the [initialization frame](/docs/voice/tts/websocket-streaming#2-streaming): ```json { "text": " ", "voice_settings": { "voice_speed": 1.2, "emotion": "happy" } } ``` Voice settings are applied when the synthesis worker starts and cannot be changed mid-session. **There are no common voice_settings fields.** Every field is provider-specific — the available fields, defaults, and accepted values are completely different per provider. Unrecognized fields are silently ignored. See your selected provider's page under [Providers](/docs/voice/tts/providers/telnyx) for the exact fields. --- ### Messages > Source: https://developers.telnyx.com/docs/voice/tts/websocket-streaming/messages.md ## Client → Server All client messages are JSON text frames. ### Text Frame Text to synthesize. `" "` (single space) for handshake. `""` (empty string) for end-of-sequence. Provider-specific voice configuration. Only used in the handshake frame (`{"text": " "}`). See [Voice Settings](/docs/voice/tts/websocket-streaming/parameters/voice-settings). When `true`, immediately synthesizes all buffered text without waiting for a sentence boundary. Default: `false`. When `true`, stops the current synthesis worker and starts a new one. The original handshake is replayed automatically. Use for barge-in/interruption. ### Message Sequence **1. Handshake** (required first message): ```json {"text": " "} ``` With optional voice settings: ```json { "text": " ", "voice_settings": { "voice_speed": 1.2 } } ``` **2. Text** (one or more): ```json {"text": "Hello, welcome to Telnyx."} ``` **3. Flush** (optional — force synthesis of buffered partial sentences): ```json {"text": "incomplete fragment", "flush": true} ``` **4. Interrupt** (optional — restart synthesis): ```json {"force": true} ``` **5. End of sequence**: ```json {"text": ""} ``` --- ## Server → Client All server messages are JSON text frames. ### Audio Chunk Returned when synthesis produces audio for a complete sentence. ```json { "audio": "", "text": "Hello, welcome to Telnyx.", "isFinal": false, "cached": false, "timeToFirstAudioFrameMs": 245 } ``` Base64-encoded audio data. `null` when the provider uses streamed delivery — audio arrives in separate streamed chunk frames instead. See note below. The text segment this audio corresponds to. `null` for streamed audio chunks. `false` for audio chunks. `true` if audio was served from cache. Time in milliseconds from speech request to first audio frame. Only present on the first chunk of each synthesis. ### Streamed Audio Chunk For providers that stream audio incrementally (Telnyx Natural, NaturalHD, Qwen3TTS, Rime, Minimax, Resemble, Inworld), audio arrives in separate frames: ```json { "audio": "", "text": null, "isFinal": false, "cached": false } ``` These contain raw audio data (`text` is always `null`). The concatenated audio chunk for these providers has `audio: null` — only the streamed chunks carry audio bytes. For AWS Polly and Azure, audio is returned in the `audio` field of the regular audio chunk frame. For all other providers, ignore the `audio` field on the text-bearing chunk and collect audio from the streamed frames. ### Final Frame Signals that synthesis is complete for the current text input: ```json { "audio": null, "text": "", "isFinal": true } ``` The connection remains open after a final frame — send more text or close. ### Error Frame ```json { "error": "Provider error message" } ``` The connection closes shortly after an error frame. --- ### Errors > Source: https://developers.telnyx.com/docs/voice/tts/websocket-streaming/errors.md ## HTTP Errors (Handshake) These occur during the WebSocket upgrade request, before the connection is established. | Code | Cause | |------|-------| | 400 | Invalid parameters — unsupported provider, missing required fields, or invalid voice format | | 401 | Missing or invalid API key | | 403 | Ultra model restricted on public WebSocket endpoint. Use [REST API](/docs/voice/tts/rest-api) for Ultra. | | 403 | Cloned voice restricted — organization requires identity verification for cloned voices (Qwen3TTS, Minimax clones) | ## WebSocket Errors (Runtime) After the connection is established, errors arrive as JSON frames: ```json { "error": "Error in audio response" } ``` The connection closes shortly after an error frame. ### Error Messages | Error | Cause | |-------|-------| | `"Error in audio response"` | The TTS provider returned an error during synthesis | | `"Error in remaining audio response"` | Provider error while synthesizing buffered text during connection close | ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | Connection rejected (400) | Invalid voice format | Use `Provider.Model.VoiceId` format (e.g., `Telnyx.NaturalHD.astra`) | | Connection rejected (401) | Missing auth | Pass `Authorization: Bearer ` header during WebSocket upgrade | | No audio after connecting | Missing handshake | Send `{"text": " "}` as first frame | | `audio` field is `null` | Expected behavior | For streamed providers (Telnyx, Rime, Minimax, Resemble, Inworld), audio arrives in separate streamed frames | | Text sent but no response | Sentence buffering | Text is buffered until a sentence boundary. Send more text, use `flush: true`, or end with punctuation | | Ultra not working on WebSocket | Intentional restriction | Ultra is REST-only. Use `POST /v2/text-to-speech/speech` | | Cloned voice rejected | Identity verification required | Complete L2 verification in the [Telnyx Portal](https://portal.telnyx.com) | --- ### Examples > Source: https://developers.telnyx.com/docs/voice/tts/websocket-streaming/examples.md ## Basic Streaming ```python Python import asyncio import json import base64 import websockets async def tts_stream(): url = "wss://api.telnyx.com/v2/text-to-speech/speech?voice=Telnyx.NaturalHD.astra" headers = {"Authorization": "Bearer YOUR_API_KEY"} async with websockets.connect(url, additional_headers=headers) as ws: # 1. Handshake await ws.send(json.dumps({"text": " "})) # 2. Send text await ws.send(json.dumps({"text": "Hello from Telnyx text-to-speech."})) # 3. Signal end of input await ws.send(json.dumps({"text": ""})) # 4. Collect audio audio_chunks = [] async for message in ws: data = json.loads(message) if data.get("error"): print(f"Error: {data['error']}") break if data.get("audio"): audio_chunks.append(base64.b64decode(data["audio"])) if data.get("isFinal"): break # Save audio with open("output.mp3", "wb") as f: for chunk in audio_chunks: f.write(chunk) asyncio.run(tts_stream()) ``` ```javascript JavaScript const WebSocket = require('ws'); const fs = require('fs'); const url = 'wss://api.telnyx.com/v2/text-to-speech/speech?voice=Telnyx.NaturalHD.astra'; const ws = new WebSocket(url, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const audioChunks = []; ws.on('open', () => { // 1. Handshake ws.send(JSON.stringify({ text: ' ' })); // 2. Send text ws.send(JSON.stringify({ text: 'Hello from Telnyx text-to-speech.' })); // 3. Signal end of input ws.send(JSON.stringify({ text: '' })); }); ws.on('message', (raw) => { const data = JSON.parse(raw); if (data.error) { console.error('Error:', data.error); ws.close(); return; } if (data.audio) { audioChunks.push(Buffer.from(data.audio, 'base64')); } if (data.isFinal) { fs.writeFileSync('output.mp3', Buffer.concat(audioChunks)); ws.close(); } }); ``` ## Conversational (Barge-In) Send multiple text segments and interrupt mid-synthesis: ```python import asyncio import json import base64 import websockets async def conversational_tts(): url = "wss://api.telnyx.com/v2/text-to-speech/speech?voice=Telnyx.NaturalHD.astra" headers = {"Authorization": "Bearer YOUR_API_KEY"} async with websockets.connect(url, additional_headers=headers) as ws: # Handshake with voice settings await ws.send(json.dumps({ "text": " ", "voice_settings": {"voice_speed": 1.1} })) # Send first sentence await ws.send(json.dumps({"text": "Welcome to the demo."})) # Wait for first audio, then interrupt async for message in ws: data = json.loads(message) if data.get("isFinal"): break # Interrupt and send new text await ws.send(json.dumps({"force": true})) await ws.send(json.dumps({"text": "Actually, let me start over."})) # Collect remaining audio... await ws.send(json.dumps({"text": ""})) async for message in ws: data = json.loads(message) if data.get("isFinal"): break asyncio.run(conversational_tts()) ``` ## LLM Token Streaming Stream tokens from an LLM directly to TTS. The server buffers text and synthesizes at sentence boundaries: ```python import asyncio import json import websockets async def llm_to_tts(llm_token_stream): url = "wss://api.telnyx.com/v2/text-to-speech/speech?voice=Telnyx.NaturalHD.astra" headers = {"Authorization": "Bearer YOUR_API_KEY"} async with websockets.connect(url, additional_headers=headers) as ws: await ws.send(json.dumps({"text": " "})) # Stream LLM tokens directly — TTS handles sentence buffering for token in llm_token_stream: await ws.send(json.dumps({"text": token})) # Done — flush remaining await ws.send(json.dumps({"text": ""})) ``` Markdown in LLM output is automatically stripped before synthesis — headers, bold, italics, code blocks, and links are converted to plain text. --- ## REST API ### Overview > Source: https://developers.telnyx.com/docs/voice/tts/rest-api.md ## How It Works You send text in, audio streams back over the same HTTP connection. No polling, no callbacks. The response uses HTTP chunked transfer encoding — audio chunks arrive as they're synthesized. Your client can begin playback immediately without waiting for the full file. The connection stays open until synthesis completes or 30 seconds pass with no new chunks. This makes REST suitable for real-time playback, not just batch file generation. For multi-turn conversational use cases where you're continuously feeding text, use [WebSocket Streaming](/docs/voice/tts/websocket-streaming) instead. --- ## Text Preprocessing Before synthesis, text passes through two stages: 1. **Markdown stripping** — headers, bold, italics, code blocks, links, lists, emoji are converted to plain text. 2. **Pronunciation dictionary** — if `pronunciation_dict_id` is set, custom word replacements are applied. --- ## API Reference The full OpenAPI spec for these endpoints is available in the auto-generated [API Reference](/docs/voice/tts/rest-api/api-reference). Note: the OAS is currently being cleaned up — some fields and provider-specific schemas may be incomplete. --- ### Request > Source: https://developers.telnyx.com/docs/voice/tts/rest-api/request.md ## Endpoint ``` POST https://api.telnyx.com/v2/text-to-speech/speech ``` ## Example ```bash curl --request POST \ --url https://api.telnyx.com/v2/text-to-speech/speech \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "text": "Hello from Telnyx text-to-speech.", "voice": "Telnyx.NaturalHD.astra" }' ``` ## Request Body | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `text` | string | Yes | — | Text to synthesize. Markdown is automatically stripped. | | `voice` | string | Yes | — | Dot-separated voice identifier. Format: `Provider.Model.VoiceId` (e.g., `Telnyx.NaturalHD.astra`) or `Provider.VoiceId` when the provider has a single model. | | `output_type` | string | No | `binary_output` | Response format: `binary_output`, `base64_output`, or `audio_id`. | | `language` | string | No | — | BCP-47 language code (e.g., `en-US`). Supported by AWS Polly, Azure, ElevenLabs, and Inworld. Ignored by other providers. | | `text_type` | string | No | `text` | `text` or `ssml`. SSML is supported by AWS Polly and Azure. Ultra has its own [SSML emotion syntax](/docs/voice/tts/providers/telnyx/ultra#ssml-emotions). | | `voice_settings` | object | No | — | Provider-specific tuning (speed, pitch, format, emotion). Fields vary by provider — see individual [provider pages](/docs/voice/tts/providers/telnyx). | | `pronunciation_dict_id` | string | No | — | UUID of a custom pronunciation dictionary. Word replacements are applied before synthesis. | | `disable_cache` | boolean | No | `false` | Bypass the audio cache and always synthesize fresh. | --- ### Response > Source: https://developers.telnyx.com/docs/voice/tts/rest-api/response.md The `output_type` request field controls what comes back. ## Streaming Audio (default) With `output_type: "binary_output"` (or omitted), the response is raw audio over HTTP chunked transfer encoding: ``` HTTP/1.1 200 OK Content-Type: audio/mpeg Transfer-Encoding: chunked