# Text to speech

> Turn text into speech in one request with POST /v1/tts — voices, models, WAV or raw PCM, sample rates and streaming.

`POST /v1/tts` takes text and answers with the audio in the response body. Use it when you want speech now: a reply to play, a line to cache, a stream to start playing before the sentence is finished.

```bash title="curl"
curl https://api.lumavox.ai/v1/tts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Hello! Your order is on its way."}' \
  -o hello.wav
```

That is a complete request: the built-in voice, the default model, a 16-bit mono WAV at 24 kHz. The key needs the `tts` scope — see [Authentication](https://docs.lumavox.ai/authentication.md).

```js title="JavaScript"
import { writeFile } from "node:fs/promises";

const response = await fetch("https://api.lumavox.ai/v1/tts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "Hello! Your order is on its way.",
    voice_id: "default",
  }),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
await writeFile("hello.wav", Buffer.from(await response.arrayBuffer()));
```

## The request

| Field | Default | What it does |
|---|---|---|
| `text` | — | What to say. 1 to 2,000 characters. |
| `voice_id` | `default` | Who says it: an `id` from `GET /v1/voices`. |
| `model` | `eleven_flash_v2_5` | Which model speaks. Must be one of the voice's `models`. |
| `format` | `wav` | `wav` or `pcm`. |
| `sample_rate` | `null` (24000) | Hz of the returned audio: 8000, 16000, 22050, 24000 or 44100. |
| `stream` | `false` | Send the audio as it is generated. `pcm` only. |
| `provider_options` | `null` | Voice settings for this request. |

**Unknown fields are refused** with a 422 that names them, so a typo in a field name never passes silently.

The full schema is in the [reference](https://docs.lumavox.ai/reference/tts.md).

## Voices

`voice_id` is an `id` from `GET /v1/voices`. That list holds:

- **the built-in voice**, `default`, a female English voice that is always available;
- **platform voices**, added by the platform and available to every organisation;
- **your organisation's own voices**, cloned from a recording or designed from a description.

```bash title="curl"
curl https://api.lumavox.ai/v1/voices \
  -H "Authorization: Bearer $API_KEY"
```

A voice your organisation added is usable once its `status` is `ready`. Until then, and for an id that is not yours, `POST /v1/tts` answers 404. See [Voices](https://docs.lumavox.ai/speech/voices.md) to add your own.

## Models

Every voice is spoken by ElevenLabs. Each voice lists the models it takes as `models` in `GET /v1/voices`; send one of them as `model`.

| `model` | Best for |
|---|---|
| `eleven_flash_v2_5` (default) | Lowest latency. Anything conversational. |
| `eleven_multilingual_v2` | Their long-standing high-quality model. |
| `eleven_v3` | Their most expressive. Slower to start, so not for an agent's turn. |

A model the voice does not list is a **422** whose issue is at `model`, and whose message names the models the voice accepts.

## Formats and sample rates

All audio from `POST /v1/tts` is 16-bit mono.

| `format` | What you get | Content type |
|---|---|---|
| `wav` | A complete WAV file, header included. | `audio/wav` |
| `pcm` | Raw 16-bit signed little-endian samples, no header. | `audio/pcm` |

`pcm` carries nothing that says what rate it is. It is 24 kHz unless you set `sample_rate` to one of 8000, 16000, 22050, 24000 or 44100. Every rate is produced natively — nothing is resampled. The `X-Sample-Rate` header always says what was actually produced; read it rather than assuming.

Playing PCM at the wrong rate plays it at the wrong speed and pitch, with no error anywhere.

For mp3 or Opus, use [stored audio](https://docs.lumavox.ai/speech/stored-audio.md): `POST /v1/audio` is queued, and asking for `mp3` or `opus` here is a 422.

## Tuning the voice

`provider_options` adjusts the voice for one request, on top of the settings the voice was registered with. Leave a field unset, or `null`, to keep the voice's own.

| Field | Range | Effect |
|---|---|---|
| `stability` | 0–1 | Low is more expressive and varies more between generations; high is flatter and repeatable. |
| `similarity_boost` | 0–1 | How closely to cling to the original voice. High can reproduce artefacts in the source. |
| `style` | 0–1 | Style exaggeration. Adds latency; leave it unset for anything conversational. |
| `use_speaker_boost` | `true` / `false` | More similarity, at the cost of latency. |
| `speed` | 0.7–1.2 | `1.0` is the voice's own pace. |

```bash title="curl"
curl https://api.lumavox.ai/v1/tts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Take a deep breath. There is no rush.",
    "model": "eleven_multilingual_v2",
    "provider_options": {"stability": 0.7, "speed": 0.9}
  }' \
  -o calm.wav
```

## Streaming

Set `"stream": true` with `"format": "pcm"` and the audio arrives as it is generated, so playback can start after the first chunk instead of after the whole text.

```bash title="curl"
curl https://api.lumavox.ai/v1/tts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "This starts playing before it has finished.", "format": "pcm", "stream": true}' \
  --no-buffer -o speech.pcm
```

**Only `pcm` can stream.** A WAV header declares the total length of the audio, which is not known until all of it exists. `"stream": true` with `wav` is a 422.

```js title="JavaScript"
const response = await fetch("https://api.lumavox.ai/v1/tts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ text: "Streaming speech.", format: "pcm", stream: true }),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));

let carry = new Uint8Array(0);
for await (const chunk of response.body) {
  // A chunk can end half way through a sample: carry the odd byte over.
  const bytes = new Uint8Array(carry.length + chunk.length);
  bytes.set(carry);
  bytes.set(chunk, carry.length);
  const whole = bytes.length - (bytes.length % 2);
  carry = bytes.slice(whole);
  const samples = new Int16Array(bytes.buffer.slice(0, whole));
  play(samples); // your player, at 24000 Hz unless you set sample_rate
}
```

What to know about a stream:

- **The status comes after the first chunk.** If speech cannot start at all you get an error status, never an empty 200.
- **A failure after that cannot change the status.** The body ends early. Compare what you received against what you expected if a cut-off matters to you.
- **The timing headers are left out.** A stream sends `Content-Type: audio/pcm`, `X-Model`, `X-Sample-Rate` and `X-Stream-Chunking: none`.

## Response headers

A whole (non-streamed) response carries:

| Header | Meaning |
|---|---|
| `X-Request-Id` | This request's id. Include it when you report a problem. |
| `X-Model` | The model that spoke. |
| `X-Sample-Rate` | Hz of the returned audio. |
| `X-Audio-Seconds` | Length of the audio. |
| `X-Generation-Seconds` | Time spent producing the speech, network included. |
| `X-Total-Seconds` | Time the request spent with us, end to end. |

## Errors

Errors use the common envelope described in [Errors and limits](https://docs.lumavox.ai/errors.md). The ones specific to this endpoint:

| Status | When |
|---|---|
| 400 | The body is not valid JSON. |
| 402 | The organisation is out of credits. |
| 404 | `voice_id` is unknown, not yours, or not `ready` yet. |
| 422 | The body failed validation: `text` empty or over 2,000 characters, an unknown field, a format other than `wav` or `pcm`, `stream` with `wav`, or a `model` that is not one of the voice's `models`. `issues` names the field. |
| 429 | Over your organisation's rate limit. |
| 502 | The speech provider failed. Safe to retry. |
| 503 | Voice lookup is briefly unavailable — retry, or use `voice_id: "default"`, which needs no lookup — or speech is not set up on this deployment. The message says which. |

## Related

- [Stored audio](https://docs.lumavox.ai/speech/stored-audio.md) — mp3 and Opus, up to 5,000 characters, kept in your library.
- [Voices](https://docs.lumavox.ai/speech/voices.md) — clone or design your own.
- [Usage](https://docs.lumavox.ai/usage.md) — what speech has cost.
- [Reference: tts](https://docs.lumavox.ai/reference/tts.md)
