# Batch TTS API

`POST /tts/speak` synthesises speech from text and streams the audio back — standalone, no call required. Same API key, same balance as your agents. **No agent required** — you pick the provider and voice directly.

## Synthesise speech

```
POST /tts/speak
```

```json
{
  "text": "Your appointment is confirmed for Friday at 2pm.",
  "voice_id": "autumn",
  "provider": "groq",
  "model": null,
  "language": "en",
  "rate": 1.0,
  "volume": 1.0,
  "format": "mp3"
}
```

| field | type | notes |
|---|---|---|
| `text` | string | **required** — 1–8000 characters |
| `voice_id` | string | **required** — a voice id from `GET /tts/voices` |
| `provider` | string | `groq` (default), `edge`, `elevenlabs`, `deepgram`, `inworld`, `voxtral`. Unknown → `422` |
| `model` | string | optional — defaults to the provider's primary model |
| `language` | string | `en` or `fr` |
| `rate` | number | `0.5`–`2.0`, applied by providers that support it |
| `volume` | number | `0.0`–`2.0`, applied by providers that support it |
| `format` | string | `mp3` (default), `wav`, or `flac` |

The response is a chunked audio stream with the matching `Content-Type` (`audio/mpeg` for mp3, etc.). Two response headers:

| header | meaning |
|---|---|
| `X-Wakili-Cache` | `hit` or `miss` — was this served from the cache? |
| `X-Wakili-Cost-Usd` | amount charged for this request (`0` on a cache hit) |

## Caching (not determinism)

TTS providers do not guarantee byte-identical audio for the same input across calls, so Wakili does not promise deterministic output. Instead it caches: the first synthesis for a given `(provider, model, voice_id, rate, volume, format, text)` tuple is stored, and every later request with the exact same tuple replays the stored audio. **A cache hit is not billed** (`X-Wakili-Cache: hit`, `X-Wakili-Cost-Usd: 0`). If you need the same audio twice, request it with identical parameters and the second call is free.

## List voices

```
GET /tts/voices?language=en
```

```json
{
  "language": "en",
  "voices": {
    "groq": ["autumn", "diana", "hannah", "austin", "daniel", "troy"],
    "elevenlabs": ["21m00Tcm4TlvDq8ikWAM", "..."]
  }
}
```

Omit `language` for the full list. Providers whose model is multilingual are always included; single-language voices are filtered to the requested language.

## Billing

You pay the provider's **list price** for the character count, plus a flat **10% routing fee**:

```
cost_usd = upstream_cost_usd × 1.10
```

`upstream_cost_usd` is the per-1000-character list price for the chosen provider. Debited from the same balance as calls. `402` with `code: "insufficient_balance"` if the balance can't cover it (checked before synthesis). A cache hit skips billing entirely.

## Errors

| status | when |
|---|---|
| `401` | missing/invalid API key |
| `402` | insufficient balance |
| `422` | invalid body (text length, missing `voice_id`, unknown `provider`, bad format) |
| `502` | upstream TTS provider error |
