> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tabbly.io/llms.txt
> Use this file to discover all available pages before exploring further.

# TTS Streaming API

> Streaming Text-to-Speech API for real-time voice synthesis

## Overview

Tabbly TTS provides a streaming Text-to-Speech API that allows you to generate high-quality voice audio in real-time for your voice AI applications. The API streams audio as it's generated, providing low-latency responses for real-time use cases.

## Base URL

```
https://api.tabbly.io
```

## Endpoint

<ApiMethod method="post" path="/tts/stream">
  Streaming Text-to-Speech endpoint that returns audio in real-time as WAV format. The API streams audio chunks as they're generated, allowing for low-latency playback.
</ApiMethod>

## Authentication

All requests require an API key passed via the `X-API-Key` header.

<ParamField header="X-API-Key" type="string" required>
  Your Tabbly TTS API key
</ParamField>

## Request

<ParamField body="text" type="string" required>
  The text to convert to speech
</ParamField>

<ParamField body="voice_id" type="string" default="Ashley">
  Voice ID to use for synthesis. Default: "Ashley"
</ParamField>

<ParamField body="model_id" type="string" default="tabbly-tts">
  Model ID to use. Default: "tabbly-tts"
</ParamField>

## Response

**Content-Type:** `audio/wav` or `application/octet-stream`

**Format:** LINEAR16 PCM, 48kHz, mono

**Streaming:** Yes - audio is streamed as it's generated

**Protocol:** HTTP streaming with WAV-encoded audio chunks embedded in the stream

## Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.tabbly.io/tts/stream' \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key-here' \
  -d '{
      "text": "Hello, this is a test of the Tabbly TTS streaming API",
      "voice_id": "Ashley",
      "model_id": "tabbly-tts"
  }'
  ```

  ```python Python theme={null}
  import httpx

  async def stream_tts(text: str, api_key: str):
      url = "https://api.tabbly.io/tts/stream"
      headers = {
          "Content-Type": "application/json",
          "X-API-Key": api_key,
      }
      data = {
          "text": text,
          "voice_id": "Ashley",
          "model_id": "tabbly-tts",
      }
      
      async with httpx.AsyncClient() as client:
          async with client.stream("POST", url, json=data, headers=headers) as response:
              response.raise_for_status()
              async for chunk in response.aiter_bytes():
                  # Process audio chunk
                  yield chunk
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');

  async function streamTTS(text, apiKey) {
      const response = await axios.post(
          'https://api.tabbly.io/tts/stream',
          {
              text: text,
              voice_id: 'Ashley',
              model_id: 'tabbly-tts'
          },
          {
              headers: {
                  'Content-Type': 'application/json',
                  'X-API-Key': apiKey
              },
              responseType: 'stream'
          }
      );
      
      return response.data;
  }
  ```

  ```python Python - Complete Example with WAV Processing theme={null}
  import httpx
  import logging

  logger = logging.getLogger(__name__)

  async def stream_tts_with_wav_processing(text: str, api_key: str):
      """Stream TTS and extract PCM data from WAV chunks."""
      url = "https://api.tabbly.io/tts/stream"
      headers = {
          "Content-Type": "application/json",
          "X-API-Key": api_key,
      }
      data = {
          "text": text,
          "voice_id": "Ashley",
          "model_id": "tabbly-tts",
      }
      
      async with httpx.AsyncClient(timeout=60.0) as client:
          async with client.stream("POST", url, json=data, headers=headers) as response:
              response.raise_for_status()
              
              buffer = bytearray()
              header_skipped = False
              
              async for chunk in response.aiter_bytes():
                  if chunk:
                      buffer.extend(chunk)
                      
                      # Skip WAV header on first chunk
                      if not header_skipped and len(buffer) >= 44:
                          if buffer[:4] == b'RIFF' and buffer[8:12] == b'WAVE':
                              # Find "data" chunk and skip header
                              for i in range(12, min(len(buffer), 200)):
                                  if buffer[i:i+4] == b'data':
                                      buffer = buffer[i+8:]  # Skip "data" + size
                                      break
                              else:
                                  buffer = buffer[44:]  # Fallback
                          header_skipped = True
                      
                      # Process audio data after header
                      if header_skipped and len(buffer) > 0:
                          # Check for additional WAV files in stream
                          processed = bytearray()
                          temp = buffer
                          
                          while len(temp) > 0:
                              if len(temp) >= 12 and temp[:4] == b'RIFF' and temp[8:12] == b'WAVE':
                                  # Extract PCM from WAV
                                  data_start = None
                                  for i in range(12, len(temp)):
                                      if i + 4 <= len(temp) and temp[i:i+4] == b'data':
                                          data_start = i + 8
                                          break
                                  
                                  if data_start:
                                      # Find end of WAV (next RIFF or end)
                                      wav_end = len(temp)
                                      for i in range(data_start, len(temp) - 4):
                                          if temp[i:i+4] == b'RIFF':
                                              wav_end = i
                                              break
                                      processed.extend(temp[data_start:wav_end])
                                      temp = temp[wav_end:]
                                  else:
                                      break
                              else:
                                  processed.extend(temp)
                                  temp = bytearray()
                          
                          buffer = temp
                          if len(processed) > 0:
                              yield bytes(processed)
  ```
</CodeGroup>

## Audio Format

<ResponseField name="Sample Rate" type="integer">
  48000 Hz (fixed)
</ResponseField>

<ResponseField name="Channels" type="integer">
  1 (mono)
</ResponseField>

<ResponseField name="Bit Depth" type="integer">
  16-bit
</ResponseField>

<ResponseField name="Format" type="string">
  LINEAR16 PCM
</ResponseField>

<ResponseField name="MIME Type" type="string">
  audio/wav (stream may contain embedded WAV files)
</ResponseField>

## WAV Header Processing

The response may include WAV files embedded in the stream. When processing the stream:

1. **Detect WAV Headers**: Look for `RIFF` and `WAVE` markers
2. **Extract PCM Data**: Find the `data` chunk and extract raw PCM audio
3. **Handle Multiple WAV Files**: The stream may contain multiple WAV files
4. **Process Audio Chunks**: Handle audio data as it arrives for real-time playback

### WAV File Structure

```
RIFF (4 bytes) - "RIFF"
File Size (4 bytes)
WAVE (4 bytes) - "WAVE"
... (format chunk, etc.)
data (4 bytes) - "data"
Data Size (4 bytes)
[PCM Audio Data]
```

### Processing Tips

* **Skip Headers**: First 44 bytes typically contain WAV header
* **Extract Data Chunk**: Look for `data` marker to find audio start
* **Handle Multiple Files**: Stream may contain multiple WAV files sequentially
* **Frame Alignment**: Ensure chunks are aligned to 16-bit sample boundaries (even bytes)

## Error Responses

<ResponseField name="400" type="object">
  Bad Request - Invalid parameters or missing required fields
</ResponseField>

<ResponseField name="401" type="object">
  Unauthorized - Invalid or missing API key
</ResponseField>

<ResponseField name="402" type="object">
  Payment Required - Insufficient wallet balance
</ResponseField>

<ResponseField name="500" type="object">
  Server error
</ResponseField>

## Rate Limits

API rate limits apply to prevent abuse. Contact support if you need higher limits.

## Best Practices

<AccordionGroup>
  <Accordion title="Stream Processing">
    Process audio chunks as they arrive rather than waiting for the complete response. This reduces latency and enables real-time playback.
  </Accordion>

  <Accordion title="WAV Processing">
    The API may send WAV files embedded in the stream. Always extract PCM data from WAV chunks for proper playback. See code examples above.
  </Accordion>

  <Accordion title="Error Handling">
    Always handle HTTP errors and network timeouts gracefully. Implement retry logic for transient failures.
  </Accordion>

  <Accordion title="Voice Selection">
    Choose appropriate voice\_id based on your use case. Different voices may have different characteristics and languages.
  </Accordion>

  <Accordion title="Text Length">
    For very long texts, consider splitting into smaller chunks for better streaming performance and lower latency.
  </Accordion>

  <Accordion title="Connection Reuse">
    Reuse HTTP client connections when making multiple requests to improve performance and reduce connection overhead.
  </Accordion>

  <Accordion title="Buffering">
    For real-time playback, implement buffering (10-20ms) to smooth out network jitter and prevent audio artifacts.
  </Accordion>

  <Accordion title="Frame Alignment">
    Ensure audio chunks are aligned to 16-bit sample boundaries (even number of bytes) to prevent audio clicks or pops.
  </Accordion>
</AccordionGroup>

## Troubleshooting

### No Audio Output

* Verify API key is correct and has sufficient wallet balance
* Check network connectivity to `https://api.tabbly.io`
* Review response status code (should be 200)
* Verify WAV headers are being detected and processed correctly
* Check logs for HTTP connection errors

### Audio Quality Issues

* Ensure sample rate matches (48000 Hz)
* Verify WAV header parsing is working correctly
* Check audio data format (should be LINEAR16 PCM)
* Ensure frame alignment (even number of bytes per chunk)
* Check for proper PCM extraction from WAV files

### Performance Issues

* Reuse HTTP client instances for better performance
* Monitor API response times
* Consider caching for repeated text
* Implement proper buffering for real-time playback
* Check network latency to API endpoint

### WAV Processing Issues

* Verify WAV headers are being detected (`RIFF` and `WAVE` markers)
* Check if `data` chunk is being found correctly
* Ensure multiple WAV files in stream are handled properly
* Verify PCM data extraction is working
* Check for incomplete WAV files (keep in buffer until complete)

## Integration Examples

### Real-Time Playback

For real-time playback, implement buffering to smooth out network jitter:

```python theme={null}
import asyncio
import httpx

async def stream_with_buffering(text: str, api_key: str, output_queue):
    """Stream TTS with buffering for smooth playback."""
    url = "https://api.tabbly.io/tts/stream"
    headers = {"Content-Type": "application/json", "X-API-Key": api_key}
    data = {"text": text, "voice_id": "Ashley", "model_id": "tabbly-tts"}
    
    buffer = bytearray()
    CHUNK_SIZE = 960  # 10ms at 48kHz
    
    async with httpx.AsyncClient() as client:
        async with client.stream("POST", url, json=data, headers=headers) as response:
            response.raise_for_status()
            
            async for chunk in response.aiter_bytes():
                buffer.extend(chunk)
                # Process WAV headers and extract PCM...
                # When buffer >= CHUNK_SIZE, push to output_queue
                while len(buffer) >= CHUNK_SIZE:
                    await output_queue.put(buffer[:CHUNK_SIZE])
                    buffer = buffer[CHUNK_SIZE:]
```

## Next Steps

* Learn how to integrate with LiveKit: [LiveKit Integration](/tts-api/tts-livekit-integration)
* Review best practices: [Best Practices](/tts-api/best-practices)
* Get your API key from the Tabbly dashboard
* Review example implementations in the code samples above
