---
title: "Character voice design API"
description: "Design a Studio character voice, read its exact job and candidates, select before expiry, or request cancellation. Retain original mutation/request/job identities. Voice creation does not generate speech; assignment_outcome and the current document determine whether casting succeeded. The project preference selects the engine for future designs independently of the sample-design provider."
canonical_url: https://lyricwinter.com/docs/api/character-voices
markdown_url: https://lyricwinter.com/docs/api/character-voices.md
last_updated: 2026-11-09
status: beta
---
# LyricWinter API reference: Character voice design

> Design a Studio character voice, read its exact job and candidates, select before expiry, or request cancellation. Retain original mutation/request/job identities. Voice creation does not generate speech; assignment_outcome and the current document determine whether casting succeeded. The project preference selects the engine for future designs independently of the sample-design provider.

The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json.

## Endpoints

- [Read the engine for future character voices](https://lyricwinter.com/docs/api/character-voices#get-studio-new-voice-engine): `GET /studio/projects/{projectId}/new-voice-engine`
- [Choose the engine for future character voices](https://lyricwinter.com/docs/api/character-voices#update-studio-new-voice-engine): `PATCH /studio/projects/{projectId}/new-voice-engine`
- [Find an active character voice design](https://lyricwinter.com/docs/api/character-voices#get-latest-studio-voice-design): `GET /studio/projects/{projectId}/speakers/{speakerId}/voice-materialization-jobs`
- [Design or prepare a character voice](https://lyricwinter.com/docs/api/character-voices#create-studio-voice-materialization): `POST /studio/projects/{projectId}/speakers/{speakerId}/voice-materialization-jobs`
- [Read a character voice job](https://lyricwinter.com/docs/api/character-voices#get-studio-voice-materialization): `GET /studio/voice-materialization-jobs/{jobId}`
- [Cancel a character voice job](https://lyricwinter.com/docs/api/character-voices#cancel-studio-voice-materialization): `POST /studio/voice-materialization-jobs/{jobId}/cancel`
- [Select a character voice candidate](https://lyricwinter.com/docs/api/character-voices#select-studio-voice-candidate): `POST /studio/voice-materialization-jobs/{jobId}/selection`

### Read the engine for future character voices

`GET /studio/projects/{projectId}/new-voice-engine`

Scope: `studio:read` · Operation ID: `getStudioNewVoiceEngine`

Read the project's current new-voice synthesis engine. Null means Automatic. This is separate from the sample-design provider and does not change voices or create audio. Requires project.read.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string, uuid | Yes |  |

#### Response 200

Project engine preference; null means Automatic.

Fields inside `data`:

- `schema_version` (string, one of `studio-new-voice-engine-1`; required)
- `model` (string | null, one of `fishaudio-drama-3`, `eleven_v4`, `inworld-2`, `cartesia-sonic-3.6`, `lightning_v3.1`, `fishaudio-s2.1-pro`, `inworld-1.5-max`, `inworld-1.5-mini`, `cartesia-sonic-3.5`, `fishaudio`, `f5tts`; required)

Errors: `400`, `401`, `403`, `500` return the standard error envelope.

#### Example request

```bash
curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/new-voice-engine" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY"
```

### Choose the engine for future character voices

`PATCH /studio/projects/{projectId}/new-voice-engine`

Scope: `studio:write` · Operation ID: `updateStudioNewVoiceEngine`

Save an eligible engine preference, or null for Automatic, with project.manage permission. Future designs capture it; existing voices and audio remain unchanged. This setter has no versioned mutation receipt: after uncertainty read and reconcile rather than automatically overwriting an intervening edit.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string, uuid | Yes |  |

#### Request body (application/json, required)

- `schema_version` (string, one of `studio-new-voice-engine-1`; required)
- `model` (string | null, one of `fishaudio-drama-3`, `eleven_v4`, `inworld-2`, `cartesia-sonic-3.6`, `lightning_v3.1`, `fishaudio-s2.1-pro`, `inworld-1.5-max`, `inworld-1.5-mini`, `cartesia-sonic-3.5`, `fishaudio`, `f5tts`; required)

#### Response 200

Saved project engine preference.

Fields inside `data`:

- `schema_version` (string, one of `studio-new-voice-engine-1`; required)
- `model` (string | null, one of `fishaudio-drama-3`, `eleven_v4`, `inworld-2`, `cartesia-sonic-3.6`, `lightning_v3.1`, `fishaudio-s2.1-pro`, `inworld-1.5-max`, `inworld-1.5-mini`, `cartesia-sonic-3.5`, `fishaudio`, `f5tts`; required)

Errors: `400`, `401`, `403`, `500` return the standard error envelope.

#### Example request

```bash
curl -X PATCH "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/new-voice-engine" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "schema_version": "studio-new-voice-engine-1",
  "model": "fishaudio-drama-3"
}'
```

### Find an active character voice design

`GET /studio/projects/{projectId}/speakers/{speakerId}/voice-materialization-jobs`

Scope: `studio:read` · Operation ID: `getLatestStudioVoiceDesign`

Return the latest queued, running or awaiting-selection AI voice design for this project/speaker, or null. This is not an original-operation lookup or proof that prior work failed; retain and read an exact confirmed job ID for recovery.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string, uuid | Yes |  |
| `speakerId` | string, uuid | Yes |  |

#### Response 200

Active voice design or null when none is active.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

Errors: `400`, `401`, `403`, `500` return the standard error envelope.

#### Example request

```bash
curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/speakers/$SPEAKER_ID/voice-materialization-jobs" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY"
```

### Design or prepare a character voice

`POST /studio/projects/{projectId}/speakers/{speakerId}/voice-materialization-jobs`

Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `createStudioVoiceMaterialization`

Submit the canonical voice materialization request with its original client_mutation_id. New AI designs use schema_version 2 and optional voice_direction; provider preparation and legacy v1 remain supported. Manual mode exposes up to three candidates; automatic mode requires one. The ordinary server captures authority and the project's casting engine. Recover only the identical original request after uncertainty; never substitute another voice or mutation. This operation does not generate speech.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string, uuid | Yes |  |
| `speakerId` | string, uuid | Yes |  |

#### Request body (application/json, required)

- One of:
  - **schema_version: 1, materialization_kind: "provider_asset"**
    - `schema_version` (1; required)
    - `client_mutation_id` (string, uuid; required)
    - `materialization_kind` ("provider_asset"; optional)
    - `expected_source_voice_id` (string, uuid; optional)
    - `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
    - `model` (any, one of `inworld-2`, `cartesia-sonic-3.5`, `fishaudio-s2.1-pro`, `lightning_v3.1`, `eleven_v4`, `phonon-31m-en`; required)
  - **schema_version: 1, materialization_kind: "ai_voice_creation"**
    - `schema_version` (1; required)
    - `client_mutation_id` (string, uuid; required)
    - `materialization_kind` ("ai_voice_creation"; required)
    - `section_id` (string, uuid; required)
    - `actor_session_id` (string, uuid; required)
    - `provider` (any, one of `inworld`, `fish_audio`; required)
    - `model` (any, one of `inworld-2`, `fishaudio-s2.1-pro`; required)
    - `selection_mode` (any, one of `automatic`, `manual`; required)
    - `candidate_count` (integer, 1–3; required)
    - `custom_prompt` (string, 30–250 chars; optional)
  - **schema_version: 2, materialization_kind: "ai_voice_creation"**
    - `schema_version` (2; required)
    - `client_mutation_id` (string, uuid; required)
    - `materialization_kind` ("ai_voice_creation"; required)
    - `section_id` (string, uuid; required)
    - `actor_session_id` (string, uuid; required)
    - `provider` (any, one of `inworld`, `fish_audio`; required)
    - `model` (any, one of `inworld-2`, `fishaudio-s2.1-pro`; required)
    - `selection_mode` (any, one of `automatic`, `manual`; required)
    - `candidate_count` (integer, 1–3; required)
    - `voice_direction` (string, 30–250 chars; optional)

#### Response 200

The provider voice was already ready.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

#### Response 202

Durable voice preparation accepted.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

Errors: `400`, `401`, `403`, `404`, `409`, `429`, `500`, `502` return the standard error envelope.

#### Example request

```bash
curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/speakers/$SPEAKER_ID/voice-materialization-jobs" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### Read a character voice job

`GET /studio/voice-materialization-jobs/{jobId}`

Scope: `studio:read` · Operation ID: `getStudioVoiceMaterialization`

Read the original job's status, candidate previews and expiry, output identities, cancellation and assignment_outcome. A completed voice with speaker_changed was not cast; verify the current document before generating speech. Optional wait_seconds from 0 to 25 waits for a status change; terminal or actionable states return immediately.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | string, uuid | Yes |  |

#### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `wait_seconds` | integer, 0–25 | No | Optional bounded wait for a status change. Returns immediately for terminal or actionable jobs; omitted or 0 reads immediately. Available to every Studio client. |

#### Response 200

Current voice preparation state.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope.

#### Example request

```bash
curl "https://lyricwinter.com/api/v1/studio/voice-materialization-jobs/$JOB_ID" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY"
```

### Cancel a character voice job

`POST /studio/voice-materialization-jobs/{jobId}/cancel`

Scope: `studio:write` · Operation ID: `cancelStudioVoiceMaterialization`

Request cancellation of the exact original job and read its actual state. Pending cancellation is not completed cleanup or immediate hard deletion and does not reverse existing casting. Do not create replacement work to recover an unknown response.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | string, uuid | Yes |  |

#### Response 200

Current cancelled or canceling preparation state.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope.

#### Example request

```bash
curl -X POST "https://lyricwinter.com/api/v1/studio/voice-materialization-jobs/$JOB_ID/cancel" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY"
```

### Select a character voice candidate

`POST /studio/voice-materialization-jobs/{jobId}/selection`

Scope: `studio:write` · Operation ID: `selectStudioVoiceCandidate`

Choose one exact candidate from its original job before selection expires. The server checks candidate ownership and speaker authority; finalization can be asynchronous. Return the actual assignment outcome, and read the same job after uncertainty. No speech generation is started.

#### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | string, uuid | Yes |  |

#### Request body (application/json, required)

- `schema_version` (1; required)
- `candidate_id` (string, uuid; required)

#### Response 200

The selected voice was already finalized.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

#### Response 202

Candidate finalization accepted.

Fields inside `data`:

- `id` (string, uuid; required)
- `project_id` (string, uuid; required)
- `section_id` (string | null, uuid; required)
- `speaker_id` (string | null, uuid; required)
- `source_voice_id` (string | null, uuid; required)
- `materialization_kind` (any, one of `provider_asset`, `ai_voice_creation`; required)
- `provider` (any, one of `inworld`, `cartesia`, `fish_audio`, `smallest_ai`, `elevenlabs`, `phonon`; required)
- `model` (string, min 1 chars; required)
- `selection_mode` (any, one of `automatic`, `manual`; required)
- `requested_candidate_count` (integer, 1–3; required)
- `status` (any, one of `queued`, `running`, `awaiting_selection`, `completed`, `failed`, `cancelled`; required)
- `selection_expires_at` (string | null; required)
- `selected_candidate_id` (string | null, uuid; required)
- `cancel_requested_at` (string | null; required)
- `output_voice_id` (string | null, uuid; required)
- `output_provider_asset_id` (string | null, uuid; required)
- `assignment_outcome` (any, one of `not_applicable`, `assigned`, `speaker_changed`; required)
- `error_code` (string | null; required)
- `error_message` (string | null; required)
- `created_at` (string; required)
- `completed_at` (string | null; required)
- `candidates` (array of object, max 3 items; required)
  - `id` (string, uuid; required)
  - `candidate_index` (integer, 0–2; required)
  - `preview_url` (string | null, uri; required)
  - `preview_text` (string, 1–500 chars; required)
  - `duration_ms` (integer | null, ≥ 0; required)
  - `status` (any, one of `available`, `selected`, `finalized`, `discarded`; required)

Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope.

#### Example request

```bash
curl -X POST "https://lyricwinter.com/api/v1/studio/voice-materialization-jobs/$JOB_ID/selection" \
  -H "Authorization: Bearer $LYRICWINTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "schema_version": 1,
  "candidate_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10"
}'
```

