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

# Speech

> The model and voice catalog a voice configuration accepts, curated phone voices, voice previews, text-to-speech, and audio transcription

The speech routes list the models and voices a [`defineVoice`](/reference/sdk/voice) configuration accepts, serve the curated list of text-to-speech voices for phone channels, synthesize a short preview of a voice, read text aloud as MP3, and transcribe an uploaded audio file. None of them is used by the CLI. The [voice](/concepts/voice) agents those models configure, their versions, outbound calls, and browser voice sessions are on [Voice](/reference/rest/voice).

*Verified against lua-cli 3.33.0.*

## Base URL and authentication

Every route sits under `https://api.heylua.ai/developer` and takes `Authorization: Bearer <<YOUR_API_KEY>>`. The preview and agent-scoped speak routes check `telephony:write` on the agent in the path; the catalog, voice list, transcription, and deprecated `speak` routes need only an authenticated key. Keys, scopes, and the error envelope are on the [REST API overview](/reference/rest/overview).

## Endpoints

### GET /developer/voice-catalog

Returns the models and voices a voice configuration accepts: voice modes, TTS provider catalogs with live voice lists, realtime engines, and the hosted LLM and STT lists. Any authenticated key; cached for 60 seconds.

**Response**

<ResponseField name="voiceModes" type="object[]">Each `{ id: "livekit-lua-agent" | "livekit-inference" | "realtime", label, tagline, description, icon, badge?, persisted: { mode, llmSource? } }`.</ResponseField>
<ResponseField name="ttsProviders" type="object[]">Each `{ id: "elevenlabs" | "cartesia" | "deepgram-aura" | "livekit-inference", label, tagline, available, error?, models: [{ id, label, tagline, latencyMs?, status }], voices: [{ id, name, provider, gender?, accent?, language?, category?, previewUrl?, enginePrefix?, iconKey?, voiceBakedInModel? }] }`. `available` is `false` when the provider's key is missing or its last fetch failed with no cached fallback; `error` says why, and can also accompany `available: true` when live discovery failed and the curated list is served. `gender` is `male`, `female`, or `neutral`.</ResponseField>
<ResponseField name="realtimeEngines" type="object[]">Each `{ id, provider: "gemini" | "openai", label, tagline, status }`; `status` is `ga`, `preview`, or `alpha`.</ResponseField>
<ResponseField name="inferenceLlms" type="object[]">Each `{ id, provider: "openai" | "google" | "xai" | "deepseek" | "kimi", label, tagline, tier: "fast" | "balanced" | "quality" }`; `id` is the model code to send, for example `openai/gpt-5-mini`.</ResponseField>
<ResponseField name="inferenceStt" type="object[]">Each `{ id, label, tagline, supportedLanguages? }`; `id` for example `deepgram/nova-3`. An absent `supportedLanguages` means every language.</ResponseField>
<ResponseField name="providerIcons" type="object">Inline SVG strings keyed by `iconKey`; absent when there are no hosted voices.</ResponseField>
<ResponseField name="generatedAt" type="string">ISO timestamp of the response.</ResponseField>

### GET /developer/voices

Returns the curated list of text-to-speech voices for phone channels. Any authenticated key.

**Response**

`200` with `{ voices: [{ id, name, gender: "male" | "female", accent, category, previewUrl? }], count }`.

### POST /developer/voices/:agentId/preview

Synthesizes a short sample of a voice and returns it inline. Scope `telephony:write` on the agent; previews count against vendor-spend limits.

<ParamField body="voiceId" type="string" required>An ElevenLabs voice ID, for example one from `GET /developer/voices`; other catalog providers are not supported here. Sent in the body because IDs can contain `/`.</ParamField>
<ParamField body="text" type="string">Text to speak; at most 500 characters. Defaults to a greeting.</ParamField>

**Response**

`201` with `{ audioUrl, format: "mp3" }`, where `audioUrl` is a `data:audio/mpeg;base64,…` URL.

### POST /developer/voices/:agentId/speak

Reads text aloud and returns the audio. Scope `telephony:write` on the agent.

<ParamField body="text" type="string" required>At most 5,000 characters.</ParamField>
<ParamField body="voiceId" type="string">Voice to use. Defaults to a server voice.</ParamField>

**Response**

`200` with the MP3 bytes, `Content-Type: audio/mpeg`, and `Content-Length`.

### POST /developer/voices/:agentId/speak/stream

Same request as `speak`, but chunked so playback can start before synthesis finishes. It uses a lower-latency model, so the audio differs subtly from `speak`; once the first byte is sent, a failure ends the stream rather than changing the status.

### POST /developer/voices/speak

Deprecated alias of `speak` without the agent in the path. Any authenticated key. Use `POST /developer/voices/:agentId/speak`.

### POST /developer/voices/transcribe

Transcribes an audio file. Any authenticated key; counts against vendor-spend limits. Send `multipart/form-data`.

<ParamField body="file" type="file" required>The audio file. Uploads over 64 MB are rejected; the server may configure a lower limit.</ParamField>
<ParamField body="language" type="string">Language hint.</ParamField>
<ParamField body="speakerSplit" type="string">`true` for a stereo recording with the microphone on the left channel and system audio on the right; the transcript is then labeled `You:` and `Others:`. Non-stereo audio falls through to the plain path.</ParamField>
<ParamField body="timestamps" type="string">`1` or `true` to include word-level timings.</ParamField>

**Response**

`201` with `{ text, languageCode, words? }`: the transcript, the detected BCP-47 language code, and, with `timestamps`, `words` as `{ w, s, e }` entries with start and end seconds.

## Errors

| Status | Message or code                                                                                                         | Meaning                                                                                                       |
| ------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `400`  | `voiceId is required`, `Voice with ID <id> not found`                                                                   | Preview without a voice, or with one that is not an ElevenLabs voice; pick an ID from `GET /developer/voices` |
| `400`  | `Preview text must be 500 characters or less`, `Text must be 5000 characters or less`, `No text to read aloud`          | Text limits; shorten or supply the text                                                                       |
| `400`  | `No audio file provided`, `Failed to transcribe audio`, `Failed to generate voice preview`, `Failed to generate speech` | Missing upload or provider failure; retry with a valid file, or later                                         |
| `413`  | `UPLOAD_TOO_LARGE`                                                                                                      | Transcription upload over the limit; the body carries `maxBytes`; split or compress the recording             |

## Example

Read a sentence aloud with the server's default voice and keep the MP3 bytes. No CLI command calls this route, so there is no CLI tab.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/voices/<<YOUR_AGENT_ID>>/speak', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ text: 'Your order has shipped and arrives on Thursday.' }),
  });
  if (!response.ok) throw new Error(`Speech failed: ${response.status} ${await response.text()}`);
  const mp3 = new Uint8Array(await response.arrayBuffer());
  console.log(response.headers.get('content-type'), mp3.byteLength);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/voices/<<YOUR_AGENT_ID>>/speak" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "text": "Your order has shipped and arrives on Thursday." }' \
    --output speech.mp3
  ```
</CodeGroup>

## See also

* [Voice](/reference/rest/voice) — voice agents, versions, dispatch, and browser voice sessions
* [Voice calls](/channels/voice-calls) — the phone channel these voices answer on
* [About voice](/concepts/voice) — voice definitions, phone numbers, and live voice sessions
* [`lua voice`](/reference/cli/voice) — try a voice from the terminal, browser, or phone
* [`lua channels`](/reference/cli/channels) — bind a number to the voice that answers
