> ## Documentation Index
> Fetch the complete documentation index at: https://60db.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# TTS WebSocket

> Text-to-Speech WebSocket endpoint for real-time audio synthesis

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# TTS WebSocket

Real-time Text-to-Speech synthesis via WebSocket streaming with full-duplex bidirectional communication.

## Endpoint

<ParamField name="url" type="string">
  `ws://api.60db.ai/ws/tts`
</ParamField>

## Authentication

Query parameter authentication:

<ParamField name="apiKey" type="string" required>
  Your API key for authentication
</ParamField>

Example:

```
ws://api.60db.ai/ws/tts?apiKey=sk_live_your_api_key
```

## Protocol Overview

```
Client                                  Server
  |                                       |
  |─── create_context ──────────────────▶ |
  |◀── context_created ─────────────────  |
  |                                       |
  |─── send_text ───────────────────────▶ |
  |─── send_text ───────────────────────▶ |
  |─── flush_context ───────────────────▶ |
  |◀── audio_chunk #1 ──────────────────  |
  |◀── audio_chunk #2 ──────────────────  |
  |◀── audio_chunk #N ──────────────────  |
  |◀── flush_completed ─────────────────  |
  |                                       |
  |─── close_context ───────────────────▶ |
  |◀── context_closed ──────────────────  |
  |          (connection closes)          |
```

## Connection Sequence

### 1. Connect

```javascript theme={null}
const ws = new WebSocket('ws://api.60db.ai/ws/tts?apiKey=sk_live_your_key');
```

### 2. Receive Authentication Message

<ResponseExample>
  ```json Response theme={null}
  {
    "connecting": true,
    "message": "Authenticating...",
    "timestamp": 1775465918269
  }
  ```
</ResponseExample>

### 3. Receive Connection Established

<ResponseExample>
  ```json Response theme={null}
  {
    "connection_established": {
      "service": "tts",
      "user_id": 43,
      "credit_balance": 9.97,
      "workspace": "default"
    }
  }
  ```
</ResponseExample>

**Fields:**

<ResponseField name="service" type="string">
  Service name: `"tts"`
</ResponseField>

<ResponseField name="user_id" type="integer">
  Your user ID
</ResponseField>

<ResponseField name="credit_balance" type="number">
  Available credits
</ResponseField>

<ResponseField name="workspace" type="string">
  Workspace name
</ResponseField>

## Client → Server Messages

### 1. create\_context

**Must be the first message.** Initializes the TTS session with voice and audio settings.

<RequestExample>
  ```json Request theme={null}
  {
    "create_context": {
      "context_id": "my-session-123",
      "voice_id": "7911a3e8",
      "audio_config": {
        "audio_encoding": "LINEAR16",
        "sample_rate_hertz": 16000
      },
      "speaking_rate": 1.0,
      "pitch": 0.0,
      "stability": 1.0,
      "boost_volume": false
    }
  }
  ```
</RequestExample>

**Parameters:**

<ParamField name="context_id" type="string">
  Unique session identifier. Default: auto-generated UUID
</ParamField>

<ParamField name="voice_id" type="string" required>
  Voice ID to use for synthesis
</ParamField>

<ParamField name="audio_config.audio_encoding" type="string" default="LINEAR16">
  Audio encoding. Options: `LINEAR16`, `PCM`, `MULAW`, `ULAW`, `OGG_OPUS`
</ParamField>

<ParamField name="audio_config.sample_rate_hertz" type="integer" default="16000">
  Sample rate in Hz. Options: `8000`, `16000`, `24000`, `48000`
</ParamField>

<ParamField name="speaking_rate" type="number" default="1.0">
  Speed multiplier (0.5 – 1.5). Values outside this range are clamped.
</ParamField>

<ParamField name="pitch" type="number" default="0.0">
  Pitch shift in semitones (-6.0 to 6.0). Values outside this range are clamped.
</ParamField>

<ParamField name="stability" type="number" default="1.0">
  Voice stability (0.6 = most expressive, 1.5 = most consistent). Controls generation temperature internally.
</ParamField>

<ParamField name="boost_volume" type="boolean" default="false">
  When `true`, normalizes output audio to -14 LUFS (broadcast loudness standard).
</ParamField>

**Supported encoding + sample rate combinations:**

Not all combinations are valid. The table below shows which pairs are supported. Unsupported combinations silently fall back to `LINEAR16` at `16000` Hz.

| `audio_encoding` | Supported `sample_rate_hertz`               | Output format                              |
| ---------------- | ------------------------------------------- | ------------------------------------------ |
| `LINEAR16`       | `8000`, `16000` (default), `24000`, `48000` | Raw PCM, 16-bit signed little-endian, mono |
| `PCM`            | `8000`, `16000` (default), `24000`, `48000` | Same as LINEAR16                           |
| `MULAW`          | `8000`                                      | G.711 μ-law encoded, mono                  |
| `ULAW`           | `8000`                                      | Same as MULAW                              |
| `OGG_OPUS`       | `24000`                                     | Ogg Opus compressed audio                  |

> **Note:** `MULAW`/`ULAW` only works at `8000` Hz. Using other sample rates with MULAW falls back to LINEAR16 @ 16kHz. Similarly, `OGG_OPUS` only works at `24000` Hz.

**Limits:**

| Parameter                 | Min    | Max          | Default | Behavior when out of range |
| ------------------------- | ------ | ------------ | ------- | -------------------------- |
| `speaking_rate`           | 0.5    | 1.5          | 1.0     | Silently clamped           |
| `pitch`                   | -6.0   | 6.0          | 0.0     | Silently clamped           |
| `stability`               | 0.6    | 1.5          | 1.0     | Silently clamped           |
| `text` (per send\_text)   | 1 char | —            | —       | Empty text is ignored      |
| text buffer (accumulated) | —      | 50,000 chars | —       | Error returned if exceeded |

### 2. send\_text

Append text to the internal buffer. Text is accumulated until a `flush_context` or `close_context` is received.

<RequestExample>
  ```json Request theme={null}
  {
    "send_text": {
      "context_id": "my-session-123",
      "text": "Hello, how are you doing today?"
    }
  }
  ```
</RequestExample>

**Fields:**

<ParamField name="context_id" type="string">
  Session identifier
</ParamField>

<ParamField name="text" type="string" required>
  Text to append to buffer (max cumulative 50,000 characters)
</ParamField>

You can send multiple `send_text` messages to build up text incrementally (e.g., from an LLM token stream):

```json theme={null}
{"send_text": {"context_id": "ctx-1", "text": "Hello, "}}
{"send_text": {"context_id": "ctx-1", "text": "how are you "}}
{"send_text": {"context_id": "ctx-1", "text": "doing today?"}}
```

**Text chunking behavior:**

Long text is automatically split into sentence-based chunks for reliable synthesis. The model works best with 3–30 second utterances. The server handles:

* Sentence boundary detection for natural chunk splits
* Newline characters (`\n`) are treated as hard paragraph boundaries
* Mixed-language text (e.g., English + Hindi) is chunked per paragraph to prevent early EOS

### 3. flush\_context

Triggers synthesis of all accumulated text. The server responds with `audio_chunk` messages followed by `flush_completed` (only on success — if synthesis fails, an `error` message is sent instead with no `flush_completed`).

<RequestExample>
  ```json Request theme={null}
  {
    "flush_context": {
      "context_id": "my-session-123"
    }
  }
  ```
</RequestExample>

### 4. close\_context

Flushes any remaining text, sends final audio, and closes the WebSocket connection.

<RequestExample>
  ```json Request theme={null}
  {
    "close_context": {
      "context_id": "my-session-123"
    }
  }
  ```
</RequestExample>

## Server → Client Messages

### context\_created

Confirms the session was initialized successfully.

<ResponseExample>
  ```json Response theme={null}
  {
    "context_created": {
      "context_id": "my-session-123"
    }
  }
  ```
</ResponseExample>

### audio\_chunk

Contains a chunk of synthesized audio. Multiple chunks are sent per flush. Each chunk is streamed as soon as it's decoded for minimum latency.

<ResponseExample>
  ```json Response theme={null}
  {
    "audio_chunk": {
      "context_id": "my-session-123",
      "audioContent": "SGVsbG8gd29ybGQ..."
    }
  }
  ```
</ResponseExample>

**Fields:**

<ResponseField name="context_id" type="string">
  Session identifier
</ResponseField>

<ResponseField name="audioContent" type="string">
  Base64-encoded audio bytes
</ResponseField>

The audio encoding and chunk format depend on `audio_config`:

| Encoding           | Chunk format                    | Notes                                                                                                                                                       |
| ------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LINEAR16` / `PCM` | Raw PCM, 16-bit signed LE, mono | Chunks can be concatenated directly                                                                                                                         |
| `MULAW` / `ULAW`   | G.711 μ-law, 8-bit, mono        | Chunks can be concatenated directly                                                                                                                         |
| `OGG_OPUS`         | Independent Ogg Opus files      | Each chunk is a self-contained OGG file. **Chunks cannot be naively concatenated** — decode each independently or use LINEAR16 for concatenatable streaming |

### flush\_completed

Signals that all audio for the flushed text has been sent. Only sent on successful synthesis — if synthesis fails, an `error` is sent instead.

<ResponseExample>
  ```json Response theme={null}
  {
    "flush_completed": {
      "context_id": "my-session-123"
    }
  }
  ```
</ResponseExample>

### context\_closed

Confirms the session is closed. The WebSocket connection closes after this message.

<ResponseExample>
  ```json Response theme={null}
  {
    "context_closed": {
      "context_id": "my-session-123"
    }
  }
  ```
</ResponseExample>

### error

Sent if synthesis fails or a protocol violation occurs.

<ResponseExample>
  ```json Response theme={null}
  {
    "error": {
      "context_id": "my-session-123",
      "message": "voice_id required"
    }
  }
  ```
</ResponseExample>

**Common errors:**

| Message                                      | Cause                                      |
| -------------------------------------------- | ------------------------------------------ |
| `voice_id required`                          | `create_context` sent without `voice_id`   |
| `text_buffer exceeded 50000 character limit` | Too much text accumulated without flushing |
| `Unsupported audio_encoding: X`              | Invalid encoding value                     |
| `Unsupported sample_rate_hertz: X`           | Invalid sample rate                        |

## Complete Example

<Tabs>
  <TabItem value="javascript" label="JavaScript (Node.js)">
    ```javascript theme={null}
    const WebSocket = require('ws');

    const API_KEY = 'sk_live_your_key';
    const ws = new WebSocket(`ws://api.60db.ai/ws/tts?apiKey=${API_KEY}`);
    const contextId = 'test-' + Date.now();

    // Store audio chunks
    const audioChunks = [];

    ws.on('open', () => {
      console.log('✓ Connected');
    });

    ws.on('message', (raw) => {
      const data = JSON.parse(raw);
      const msgType = Object.keys(data)[0];
      console.log('←', msgType);

      if (data.connection_established) {
        console.log('  Credits:', data.connection_established.credit_balance);

        // Create context
        ws.send(JSON.stringify({
          create_context: {
            context_id: contextId,
            voice_id: '7911a3e8',
            audio_config: {
              audio_encoding: 'LINEAR16',
              sample_rate_hertz: 16000
            },
            speaking_rate: 1.0,
            pitch: 0.0,
            stability: 1.0,
            boost_volume: false
          }
        }));

      } else if (data.context_created) {
        console.log('✓ Context created!');

        // Send text
        ws.send(JSON.stringify({
          send_text: {
            context_id: contextId,
            text: 'Hello, how are you doing today?'
          }
        }));

        // Flush
        ws.send(JSON.stringify({
          flush_context: { context_id: contextId }
        }));

      } else if (data.audio_chunk) {
        console.log('  Received audio chunk');
        const audioData = Buffer.from(data.audio_chunk.audioContent, 'base64');
        audioChunks.push(audioData);

      } else if (data.flush_completed) {
        console.log('✓ Flush completed!');
        console.log('  Total audio size:', audioChunks.reduce((sum, chunk) => sum + chunk.length, 0), 'bytes');

        // Close context
        ws.send(JSON.stringify({
          close_context: { context_id: contextId }
        }));

      } else if (data.context_closed) {
        console.log('✓ Context closed');

        // Save audio
        const audio = Buffer.concat(audioChunks);
        require('fs').writeFileSync('output.pcm', audio);
        console.log('Saved output.pcm');

        ws.close();
      }

      if (data.error) {
        console.error('TTS Error:', data.error.message);
      }
    });

    ws.on('error', (error) => {
      console.error('Error:', error);
    });

    ws.on('close', () => {
      console.log('Connection closed');
    });
    ```
  </TabItem>

  <TabItem value="python" label="Python">
    ```python theme={null}
    import asyncio
    import json
    import base64
    import websockets

    async def tts_websocket():
        API_KEY = "sk_live_your_key"
        url = f"ws://api.60db.ai/ws/tts?apiKey={API_KEY}"
        context_id = f"session-{int(asyncio.get_event_loop().time())}"

        async with websockets.connect(url) as ws:
            # Wait for connection
            resp = json.loads(await ws.recv())
            if resp.get('connection_established'):
                print(f"✓ Connected (Credits: ${resp['connection_established']['credit_balance']})")

            # Create context
            await ws.send(json.dumps({
                "create_context": {
                    "context_id": context_id,
                    "voice_id": "7911a3e8",
                    "audio_config": {
                        "audio_encoding": "LINEAR16",
                        "sample_rate_hertz": 16000
                    },
                    "speaking_rate": 1.0,
                    "pitch": 0.0,
                    "stability": 1.0,
                    "boost_volume": False
                }
            }))

            # Wait for context_created
            resp = json.loads(await ws.recv())
            assert "context_created" in resp
            print("✓ Context created!")

            # Send text + flush
            await ws.send(json.dumps({
                "send_text": {
                    "context_id": context_id,
                    "text": "Hello, how are you doing today?"
                }
            }))
            await ws.send(json.dumps({
                "flush_context": {"context_id": context_id}
            }))

            # Receive audio chunks until flush_completed
            audio_data = b""
            while True:
                msg = json.loads(await ws.recv())
                if "audio_chunk" in msg:
                    audio_data += base64.b64decode(msg["audio_chunk"]["audioContent"])
                    print("  Received audio chunk")
                elif "flush_completed" in msg:
                    print("✓ Flush completed!")
                    break
                elif "error" in msg:
                    print(f"Error: {msg['error']['message']}")
                    break

            # Close context
            await ws.send(json.dumps({
                "close_context": {"context_id": context_id}
            }))
            resp = json.loads(await ws.recv())
            assert "context_closed" in resp

            # Save raw PCM (16-bit, 16kHz, mono)
            with open("output.pcm", "wb") as f:
                f.write(audio_data)
            print(f"Saved {len(audio_data)} bytes to output.pcm")

    asyncio.run(tts_websocket())
    ```
  </TabItem>

  <TabItem value="browser" label="Browser">
    ```javascript theme={null}
    const ws = new WebSocket('ws://api.60db.ai/ws/tts?apiKey=sk_live_your_key');
    const contextId = 'test-' + Date.now();

    const audioChunks = [];
    let audioCtx;

    ws.onopen = () => {
      console.log('✓ Connected');
    };

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      const msgType = Object.keys(data)[0];
      console.log('←', msgType);

      if (data.connection_established) {
        console.log('  Credits:', data.connection_established.credit_balance);

        // Create context
        ws.send(JSON.stringify({
          create_context: {
            context_id: contextId,
            voice_id: '7911a3e8',
            audio_config: { audio_encoding: 'LINEAR16', sample_rate_hertz: 16000 },
            speaking_rate: 1.0,
            pitch: 0.0,
            stability: 1.0,
            boost_volume: false
          }
        }));

      } else if (data.context_created) {
        console.log('✓ Context created!');

        // Send text + flush
        ws.send(JSON.stringify({
          send_text: { context_id: contextId, text: 'Hello, how are you?' }
        }));
        ws.send(JSON.stringify({
          flush_context: { context_id: contextId }
        }));

      } else if (data.audio_chunk) {
        // Collect base64 audio
        const binary = atob(data.audio_chunk.audioContent);
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
        audioChunks.push(bytes);
        console.log('  Received audio chunk');

      } else if (data.flush_completed) {
        console.log('✓ Flush completed!');

        // Play collected audio
        playPCM16(audioChunks, 16000);

        // Close context
        ws.send(JSON.stringify({
          close_context: { context_id: contextId }
        }));
      }

      if (data.error) {
        console.error('TTS Error:', data.error.message);
      }

      if (data.context_closed) {
        console.log('✓ Context closed');
      }
    };

    function playPCM16(chunks, sampleRate) {
      const totalBytes = chunks.reduce((s, c) => s + c.length, 0);
      const merged = new Uint8Array(totalBytes);
      let offset = 0;
      for (const chunk of chunks) {
        merged.set(chunk, offset);
        offset += chunk.length;
      }

      const int16 = new Int16Array(merged.buffer);
      const float32 = new Float32Array(int16.length);
      for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;

      audioCtx = audioCtx || new AudioContext({ sampleRate });
      const buf = audioCtx.createBuffer(1, float32.length, sampleRate);
      buf.getChannelData(0).set(float32);
      const src = audioCtx.createBufferSource();
      src.buffer = buf;
      src.connect(audioCtx.destination);
      src.start();
    }
    ```
  </TabItem>
</Tabs>

## Real-time Playback (Browser)

For low-latency playback as chunks arrive (instead of waiting for all chunks), use the Web Audio API with scheduled `AudioBufferSourceNode`:

```javascript theme={null}
let audioCtx;
let nextPlayTime = 0;

function onAudioChunk(base64Audio, sampleRate) {
  const binary = atob(base64Audio);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);

  // Decode PCM int16 LE → Float32
  const int16 = new Int16Array(bytes.buffer);
  const float32 = new Float32Array(int16.length);
  for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;

  if (!audioCtx) audioCtx = new AudioContext({ sampleRate });
  const buf = audioCtx.createBuffer(1, float32.length, sampleRate);
  buf.getChannelData(0).set(float32);
  const source = audioCtx.createBufferSource();
  source.buffer = buf;
  source.connect(audioCtx.destination);

  const now = audioCtx.currentTime;
  // First chunk: 150ms pre-buffer to absorb network jitter
  // Late chunks: schedule 20ms ahead for minimal gap
  if (nextPlayTime <= now) {
    nextPlayTime = now + (nextPlayTime === 0 ? 0.15 : 0.02);
  }
  source.start(nextPlayTime);
  nextPlayTime += float32.length / sampleRate;
}
```

## LLM Integration Pattern

When streaming tokens from an LLM into TTS:

```python theme={null}
import json
import base64
import websockets

async def llm_to_tts(llm_stream, voice_id):
    API_KEY = "sk_live_your_key"
    url = f"ws://api.60db.ai/ws/tts?apiKey={API_KEY}"

    async with websockets.connect(url) as ws:
        # Wait for connection
        await ws.recv()  # connection_established

        # Create context
        await ws.send(json.dumps({
            "create_context": {
                "context_id": "llm-session",
                "voice_id": voice_id,
                "audio_config": {"audio_encoding": "MULAW", "sample_rate_hertz": 8000}
            }
        }))
        await ws.recv()  # context_created

        # Stream LLM tokens as text chunks
        async for token in llm_stream:
            await ws.send(json.dumps({
                "send_text": {"context_id": "llm-session", "text": token}
            }))

        # Flush + close when LLM is done
        await ws.send(json.dumps({
            "flush_context": {"context_id": "llm-session"}
        }))

        audio = b""
        while True:
            msg = json.loads(await ws.recv())
            if "audio_chunk" in msg:
                audio += base64.b64decode(msg["audio_chunk"]["audioContent"])
            elif "flush_completed" in msg:
                break
            elif "error" in msg:
                raise RuntimeError(msg["error"]["message"])

        await ws.send(json.dumps({
            "close_context": {"context_id": "llm-session"}
        }))
        await ws.recv()  # context_closed

        return audio
```

## Audio Format Notes

| Encoding   | Format                          | Chunk behavior                                               | Best for                            |
| ---------- | ------------------------------- | ------------------------------------------------------------ | ----------------------------------- |
| `LINEAR16` | Raw PCM, 16-bit signed LE, mono | Concatenatable                                               | General purpose, highest quality    |
| `MULAW`    | G.711 μ-law, 8kHz, mono         | Concatenatable                                               | Telephony (Twilio, SIP)             |
| `OGG_OPUS` | Ogg Opus compressed, 24kHz      | **NOT concatenatable** — each chunk is a standalone OGG file | Web playback, bandwidth-constrained |

For telephony integration (Twilio, etc.), use `MULAW` at `8000` Hz:

```json theme={null}
"audio_config": {
  "audio_encoding": "MULAW",
  "sample_rate_hertz": 8000
}
```

For web playback with low bandwidth, use `OGG_OPUS` at `24000` Hz:

```json theme={null}
"audio_config": {
  "audio_encoding": "OGG_OPUS",
  "sample_rate_hertz": 24000
}
```

> **Important:** OGG\_OPUS chunks are individually wrapped OGG files. To merge for download, decode each chunk independently (e.g., via `AudioContext.decodeAudioData()`) and concatenate the PCM output. Do not concatenate raw OGG bytes.

## Supported Languages

The TTS model supports synthesis in multiple Indic languages and English. The language is auto-detected from the input text — no explicit language parameter is needed.

| Language  | ID |
| --------- | -- |
| English   | en |
| Hindi     | hi |
| Bengali   | bn |
| Gujarati  | gu |
| Kannada   | kn |
| Malayalam | ml |
| Marathi   | mr |
| Punjabi   | pa |
| Tamil     | ta |
| Telugu    | te |
| Assamese  | as |
| Odia      | or |

Mixed-language text is supported. Use newlines (`\n`) to separate paragraphs in different languages for best results.

## Default Voice

The default voice ID is:

```
fbb75ed2-975a-40c7-9e06-38e30524a9a1
```

To get more voices, use the [Voices API](/api-reference/voices/get-voices).

## Context Management

### Reuse Context

Keep a context open for multiple syntheses:

```javascript theme={null}
// Create once
ws.send(JSON.stringify({
  create_context: { context_id, voice_id, audio_config }
}));

// Send multiple texts
ws.send(JSON.stringify({ send_text: { context_id, text: "Hello" } }));
ws.send(JSON.stringify({ flush_context: { context_id } }));

ws.send(JSON.stringify({ send_text: { context_id, text: "World" } }));
ws.send(JSON.stringify({ flush_context: { context_id } }));

// Close when done
ws.send(JSON.stringify({ close_context: { context_id } }));
```

### Multiple Contexts

You can create multiple contexts in one connection:

```javascript theme={null}
const context1 = 'ctx-1';
const context2 = 'ctx-2';

// Create both contexts
ws.send(JSON.stringify({
  create_context: {
    context_id: context1,
    voice_id: voice1,
    audio_config
  }
}));

ws.send(JSON.stringify({
  create_context: {
    context_id: context2,
    voice_id: voice2,
    audio_config
  }
}));
```

## Pricing

* **Rate**: \$0.00002 per character
* **Minimum**: \$0.01 per context
* **Billing**: Per character synthesized

## Error Codes

| Code | Description           |
| ---- | --------------------- |
| 1008 | Authentication failed |
| 1008 | Insufficient credits  |
| 1011 | Voice not found       |
| 1011 | Invalid audio config  |
| 1006 | Connection lost       |

## Testing

```bash theme={null}
# Install wscat
npm install -g wscat

# Test TTS
wscat -c "ws://api.60db.ai/ws/tts?apiKey=sk_live_your_key"
```

Then send:

```json theme={null}
{"create_context":{"context_id":"test-123","voice_id":"fbb75ed2-975a-40c7-9e06-38e30524a9a1","audio_config":{"audio_encoding":"LINEAR16","sample_rate_hertz":16000}}}
{"send_text":{"context_id":"test-123","text":"Hello"}}
{"flush_context":{"context_id":"test-123"}}
```

## Related

* [STT WebSocket](/api-reference/websocket/stt) - Speech-to-Text endpoint
* [Voices API](/api-reference/voices/get-voices) - Get available voices
* [WebSocket API Reference](/websocket-api) - Complete documentation
