> ## 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.

# Voice

> Voice agents and their versions, publishing and deactivating them, outbound call dispatch, and browser voice sessions for your own front end

The voice routes manage the [voice](/concepts/voice) definitions attached to an [agent](/concepts/agents), place outbound calls or open a room a browser can join, and mint a media room and client token for a custom voice front end. A voice agent is one `defineVoice` definition on an agent; it carries versions, and the published version is the one calls run. [`lua push voice`](/reference/cli/push) calls create and version, then publishes with `--auto-deploy` or when you answer yes to its deploy prompt; `--force` alone never publishes. [`lua voice`](/reference/cli/voice) calls dispatch with a phone or web target, and `lua voice list` reads the compiled project, not this API; the voice-session route is not used by the CLI. The model and voice catalog, text-to-speech, and transcription are on [Speech](/reference/rest/speech).

*Verified against lua-cli 3.33.0.*

## Base URL and authentication

Every route sits under `https://api.heylua.ai/developer`, takes `Authorization: Bearer <<YOUR_API_KEY>>`, and checks a `telephony` scope on the agent in the path: `telephony:read` lists, `telephony:write` creates, updates, publishes, and opens voice sessions, `telephony:manage` deactivates, and `telephony:provision` places outbound calls. A `POST` that creates something answers `201`. Keys, scopes, and the error envelope are on the [REST API overview](/reference/rest/overview).

## Voice object

<ResponseField name="id" type="string">Voice ID (`voiceId` in the other routes).</ResponseField>
<ResponseField name="name" type="string">Voice name from the definition.</ResponseField>
<ResponseField name="description" type="string">Optional description.</ResponseField>
<ResponseField name="active" type="boolean">Whether the voice is enabled.</ResponseField>
<ResponseField name="source" type="string">`cli` for every voice created through this API, `provision-from-config` included; `manual` is reserved.</ResponseField>
<ResponseField name="activeVersionId" type="string">The published version string, when one exists.</ResponseField>
<ResponseField name="createdBy" type="string">User ID of the creator.</ResponseField>
<ResponseField name="createdAt, updatedAt" type="string">Timestamps.</ResponseField>
<ResponseField name="versions" type="object[]">Every pushed version: `id`, `version`, `description`, `active`, `createdAt`, `createdBy`, and every configuration field of the version body, `bundle` included.</ResponseField>

## Endpoints

Voice-agent routes sit under `/developer/voice-agents/:agentId`, where `:agentId` is the Lua agent; the voice-session route is under `/developer/voice/:agentId`.

### GET /developer/voice-agents/:agentId

Lists the agent's voices as `{ voices: Voice[] }`, each with all of its versions and their bundles. Scope `telephony:read`. `404` `Agent not found`.

### POST /developer/voice-agents/:agentId

Creates a voice. Scope `telephony:write`. Equivalent: `lua push voice` creates the voice on first push.

<ParamField body="name" type="string" required>Letters, digits, `_`, and `-` only; at most 64 characters. Unique among the agent's active voices.</ParamField>
<ParamField body="description" type="string">At most 500 characters.</ParamField>

**Response**

`201` with the stored record: `id`, `name`, `description`, `agentId`, `active`, `source`, `createdBy`, `createdAt`, `updatedAt`. `400` `A voice agent named "<name>" already exists for this agent`.

### POST /developer/voice-agents/:agentId/:voiceId/version

Pushes a new version of a voice. Scope `telephony:write`. The body is the compiled form of a [`defineVoice`](/reference/sdk/voice) configuration; this route validates each field on its own, while the pairing rules between `llm`, `stt`, and `tts` are enforced by `lua push` and by `provision-from-config`. Equivalent: `lua push voice`.

<ParamField body="version" type="string" required>`MAJOR.MINOR.PATCH` with an optional `-suffix`, at most 100 characters; otherwise `400` `Invalid semantic version: <version>`. A version that exists answers `400` `Version <version> already exists for voice agent <voiceId>`.</ParamField>
<ParamField body="description" type="string">At most 500 characters.</ParamField>
<ParamField body="llm" type="object" required>Model spec: `{ kind: "inference", model, options? }`, `{ kind: "plugin", provider: "deepgram" | "elevenlabs", class: "LLM" | "STT" | "STTv2" | "TTS", options }`, or a realtime speech-to-speech model `{ kind: "realtime", provider: "openai" | "google" | "xai", options }`.</ParamField>
<ParamField body="stt" type="object">Speech-to-text spec, same forms. Omit when `llm` is a realtime model.</ParamField>
<ParamField body="tts" type="object">Text-to-speech spec, same forms, with an optional `voice`. Omit when `llm` is a realtime model in full mode.</ParamField>
<ParamField body="vad" type="string">Voice-activity detector, for example `silero`; at most 100 characters.</ParamField>
<ParamField body="vadOptions" type="object">`{ minSpeechDuration?, minSilenceDuration?, prefixPaddingDuration?, activationThreshold? }`; stored and served, not validated here.</ParamField>
<ParamField body="turnDetection" type="string">Turn-detection mode; at most 100 characters.</ParamField>
<ParamField body="greeting" type="string">Spoken when the call connects; at most 1,000 characters.</ParamField>
<ParamField body="maxToolSteps" type="number">Tool calls allowed per turn.</ParamField>
<ParamField body="userAwayTimeout" type="number">Seconds of silence before the end user counts as away.</ParamField>
<ParamField body="preemptiveGeneration" type="boolean">Start generating before the end user's turn ends.</ParamField>
<ParamField body="interruption" type="object">`{ enabled?, mode?: "adaptive" | "vad", falseInterruptionTimeout?, resumeFalseInterruption?, minDelay?, maxDelay? }`; `400` `interruption.minDelay must be ≤ maxDelay` otherwise.</ParamField>
<ParamField body="sttLanguage" type="string">Language code for transcription; at most 100 characters.</ParamField>
<ParamField body="hasInterruption, hasOnEnter, hasOnUserTurnCompleted, hasOnExit, hasTools" type="boolean">Presence flags the compiler sets for hooks and voice tools in `bundle`.</ParamField>
<ParamField body="krispEnabled" type="boolean">Opt in to Krisp noise cancellation.</ParamField>
<ParamField body="persistTranscript" type="boolean">Store the call transcript in `Data` under `call:<sessionId>` when the call ends.</ParamField>
<ParamField body="backgroundAudio" type="object">`{ ambient?, thinking? }`: a built-in clip name, an audio config object, or an array for a probabilistic mix; not validated here.</ParamField>
<ParamField body="volume" type="number">Output volume, 0 to 100.</ParamField>
<ParamField body="pronunciations" type="object">Word replacements applied before speech, as a string-to-string map; keys at most 256 characters, values at most 1,024, 32 KB in total.</ParamField>
<ParamField body="excludeTools" type="string[]">Platform base tools to leave out of this voice.</ParamField>
<ParamField body="bundle" type="string">Compiled hooks and voice tools, gzipped and base64-encoded; at most 10 MB.</ParamField>

**Response**

`201` with the stored version record: `id`, `version`, `description`, `createdBy`, `createdAt`, `updatedAt`, and every configuration field sent. `404` `Voice agent not found`.

### GET /developer/voice-agents/:agentId/:voiceId/versions

Lists a voice's versions, newest first. Scope `telephony:read`.

**Response**

`200` with `{ versions: [{ version, createdDate, createdBy, isCurrent, createdByEmail, createdByFullName }], activeVersionId }`; `createdByEmail` and `createdByFullName` are `null`.

### PUT /developer/voice-agents/:agentId/:voiceId/:version/publish

Makes a pushed version the one calls run, and re-enables a deactivated voice. Scope `telephony:write`. Equivalent: `lua push voice --auto-deploy`.

**Response**

`200` with `{ message: "Version <version> has been published successfully", voiceId, activeVersionId, publishedAt }`. `404` `Version <version> not found for voice agent <voiceId>`.

### PUT /developer/voice-agents/:agentId/:voiceId

Renames, describes, or enables and disables a voice. Scope `telephony:write`.

<ParamField body="name" type="string">Same rule as on create.</ParamField>
<ParamField body="description" type="string">At most 500 characters.</ParamField>
<ParamField body="active" type="boolean">Enable or disable the voice.</ParamField>

**Response**

`200` with the updated voice record, without `versions`.

### DELETE /developer/voice-agents/:agentId/:voiceId

Deactivates a voice; nothing is removed, and its name becomes available to a new voice. Scope `telephony:manage`.

**Response**

`200` with `{ message: "Voice agent <name> deactivated", deleted: false, deactivated: true }`.

### POST /developer/voice-agents/:agentId/provision-from-config

Creates or reuses a voice by name, pushes the next patch version from a flat configuration, publishes it, and binds it to the agent, in one call. Scope `telephony:write`. The admin dashboard's phone-number connection uses this route; from code, prefer `lua push voice`. The body is the version body of `POST .../:voiceId/version` without `version`, plus:

<ParamField body="name" type="string">Voice name, validated as on create. Defaults to `phone-<msisdn>` with disallowed characters removed, or `phone-voice`.</ParamField>
<ParamField body="msisdn" type="string">Number used to derive the default name.</ParamField>

**Response**

`201` with `{ voiceId, version }`; store `voiceId` as the phone channel's `voiceId`. When a later step fails, a freshly created voice is deactivated again and a reused voice gets its previous published version back. `400` `Invalid voice config: …` when the configuration fails validation.

### POST /developer/voice-agents/:agentId/dispatch

Places an outbound call, or opens a room a browser can join, from a voice on the agent. Scope `telephony:provision` on the agent; at most 100 dispatches per agent per minute. Equivalent: `lua voice --phone <number>` for phone, `lua voice` for browser; from agent code, `Voice.call` in the [voice runtime](/reference/sdk/voice-runtime).

<ParamField body="to" type="string | object" required>Where to dial. An E.164 string is shorthand for `{ kind: "phone", number }`. Objects: `{ kind: "phone", number, callerId? }` (both E.164; `callerId` must be a number the agent's organization owns), `{ kind: "meet", url }` (a meeting link, at most 2,048 characters), or `{ kind: "web", returnToken?: true }` (`false` is rejected: it would open a room nobody can join).</ParamField>
<ParamField body="voice" type="string">Voice ID or name, at most 64 characters. Defaults to the agent's first active voice in creation order.</ParamField>
<ParamField body="context" type="object">JSON object seeded into the call's initial context; at most 16 KB serialized.</ParamField>
<ParamField body="threadId" type="string">Thread suffix for the call's data scope, at most 100 characters. Defaults to `voice:<sessionId>`.</ParamField>
<ParamField body="channel" type="string">Overrides the channel kind: `phone`, `meeting`, `whatsapp`, `webchat`, or `unknown`. Inferred from `to.kind` by default.</ParamField>

**Response**

`201` with `{ sessionId, roomName, callId? }` for a phone or meeting target, where `roomName` is `lua-voice-<sessionId>` and `callId` is the provider's call ID, or `{ sessionId, roomName, joinUrl }` for a web target.

### POST /developer/voice/:agentId/session

Creates a media room and a client token for your own front end to talk to the agent with a standard LiveKit client; the hosted front end is [voice chat in the web widget](/channels/web-widget/voice-chat). Scope `telephony:write`. The upstream call is bounded to 15 seconds; a timeout or transport failure answers `503` `UPSTREAM_UNAVAILABLE` with `requestId` and `retryAfterSeconds`, and any `4xx` from the voice service passes through. The SDK's `Voice.createSession` wraps this route for local runs only; see the [voice runtime](/reference/sdk/voice-runtime) for the caveat.

<ParamField body="channel" type="string" default="web">Only `web` is accepted; anything else is `400` `channel must be 'web' for developer voice sessions`.</ParamField>
<ParamField body="userId" type="string">Your end user's ID, so conversation memory and the transcript are scoped to them. Without it the voice session runs under a synthetic identity.</ParamField>
<ParamField body="voiceId" type="string">Voice to run, overriding the agent's channel-bound voice.</ParamField>
<ParamField body="displayName" type="string">Participant display name.</ParamField>

**Response**

`201` with `{ url, roomName, token, participantIdentity, agentName }`: the media server URL (`wss://`), the room to join, the client access token (JWT), the identity the token grants, and the agent participant's name in the room.

## Errors

| Status | Message or code                                                                                                                                                   | Meaning                                                                                                                             |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Validation messages                                                                                                                                               | A version or dispatch field failed a rule named in `message`, for example `interruption.minDelay must be ≤ maxDelay`; fix the field |
| `400`  | `Invalid voice config: …`                                                                                                                                         | `provision-from-config` body failed the voice schema; fix the listed issues                                                         |
| `400`  | `A voice agent named "<name>" already exists for this agent`, `Invalid semantic version: <version>`, `Version <version> already exists for voice agent <voiceId>` | Create or version refused; pick another name or version                                                                             |
| `400`  | `Agent <agentId> has no active LuaVoice configured. Push a voice before dispatching.`                                                                             | Dispatch with no active voice; push and publish one                                                                                 |
| `400`  | `channel must be 'web' for developer voice sessions`                                                                                                              | Session body with another channel; omit `channel`                                                                                   |
| `403`  | `Caller ID is not authorized for this agent.`                                                                                                                     | `callerId` is not a number the agent's organization owns; use an owned number or omit it                                            |
| `403`  | `Voice "<voice>" not active on agent <agentId> (deactivated, missing, or owned by a different agent).`                                                            | Dispatch named a voice this agent cannot use; publish it or pick another                                                            |
| `404`  | `Agent not found`, `Voice agent not found`, `Version <version> not found for voice agent <voiceId>`                                                               | Unknown agent, voice, or version; list and retry                                                                                    |
| `429`  | `Voice dispatch rate limit exceeded for agent <agentId> (<n>/min)`                                                                                                | More than 100 dispatches in a minute; wait                                                                                          |
| `503`  | `UPSTREAM_UNAVAILABLE`                                                                                                                                            | Voice session creation timed out or the upstream failed; retry after `retryAfterSeconds`                                            |

## Example

Call a customer back from the agent's first active voice with an order ID in context.

<CodeGroup>
  ```bash CLI theme={null}
  lua voice --phone +15551234567 --context '{"orderId":"ord_abc123"}'
  ```

  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/voice-agents/<<YOUR_AGENT_ID>>/dispatch', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ to: { kind: 'phone', number: '+15551234567' }, context: { orderId: 'ord_abc123' } }),
  });
  if (!response.ok) throw new Error(`Dispatch failed: ${response.status} ${await response.text()}`);
  const call: { sessionId: string; roomName: string; callId?: string } = await response.json();
  console.log(call.sessionId, call.callId);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/voice-agents/<<YOUR_AGENT_ID>>/dispatch" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "to": { "kind": "phone", "number": "+15551234567" }, "context": { "orderId": "ord_abc123" } }'
  ```
</CodeGroup>

## See also

* [Speech](/reference/rest/speech) — the model and voice catalog, previews, text-to-speech, and transcription
* [Voice calls](/channels/voice-calls) — answer and place phone calls with a voice definition
* [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
