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

# Localize a voice

> Generate localized prompt candidates that make an existing voice speak a target language while keeping the same speaker identity. Returns multiple candidates on a draft; listen to them and confirm your pick with [Approve a localization](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/approve-localization.md). Unapproved drafts expire after 3 days.

<Tip>
**Short URL Path:** You can omit `/workspaces/{workspace}` from the path. The service then derives the workspace from your API key. The full path `/voices/v1/workspaces/{workspace}/voices/{voice}:localize` is still supported.
</Tip>

Localization is a **two-call flow with a human in the middle**:

1. **Localize** (this endpoint) — generates localized prompt candidates and saves them on a hidden draft.
2. **[Approve](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/approve-localization.md)** — you listen to the candidates, pick one, and it becomes the voice's prompt for that language.

Candidates are not automatically scored — a person is expected to listen to each `previewAudio` and choose by ear.

<Tip>
Use `candidateCount` to keep the response small when you don't need every option. Candidates are ordered by historical approval rates measured across all languages, so asking for fewer gives you the engine that performs best *on average* — not necessarily the best one for your target language. When quality in a specific language matters, request more candidates and compare.
</Tip>

<Note>
**Behavior worth knowing:**
- **Drafts expire.** An unapproved draft is cleaned up after 3 days. Drafts don't appear in your voice listings.
- **Re-localizing the same voice and language replaces the draft's candidates** — candidate IDs from an earlier response stop resolving. Always approve from the most recent localize response.
- **Language limit.** A voice can hold at most 20 languages (as reported by `promptLanguages`). Localizing a voice at the cap is rejected with `400 INVALID_ARGUMENT` before any synthesis runs.
- **Region required.** `de` is rejected where `de-DE` is accepted. See [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md) for the supported catalog.
</Note>

<Note>
Localization calls consume credits and are rate limited separately from speech synthesis. A `429` means the per-tier rate limit was exceeded — safe to retry after backing off.
</Note>

## API reference

**Endpoint:** `POST https://api.inworld.ai/voices/v1/voices/{voiceId}:localize`

### Authorization

- `Authorization` (string; required) — Your [API key](../../../api-reference/introduction). Read permissions are required for GET endpoints. Write permissions are required for POST, PATCH, and DELETE endpoints.
  
   For Basic authentication, please populate `Basic $INWORLD_API_KEY`. You can create a key in one command with the [Inworld CLI](../../../developer-tools/inworld-cli): `inworld workspace add-key`.

### Path parameters

- `voiceId` (string; required) — Voice ID of the voice to localize. Expected format: `{workspace}__{voice}`.

### Request body

Content type: `application/json`

- `targetLanguage` (string; required) — The language to localize the voice into, as a BCP-47 code with a region — e.g. `de-DE`, `es-MX`, `pt-BR`. Matching is case- and separator-insensitive (`es_MX` and `ES-mx` are accepted). A bare language code with no region (`de`) is rejected, and `auto` is not supported. See [Languages](https://docs.inworld.ai/tts/capabilities/multilingual.md) for the supported catalog.
- `targetTranscription` (string) — Optional pre-translated text in the target language, up to 200 characters. When supplied, the automatic translation step is skipped and the candidates speak this text instead.
- `candidateCount` (integer) — Optional. Caps how many candidates are returned, from 1 to 3. Omit (or send `0`) to get one candidate per available synthesis engine, which is the default.
  
  Fewer candidates may come back than you ask for: the number of available engines sets the ceiling, and `candidateCount` can only lower it, never raise it.
  
  Candidates are kept in a fixed order derived from historical approval rates **across all languages**. That ranking is global. The strongest engine for any one language can differ from it. A low `candidateCount` gives you the historically best-performing engine, which may not produce the best candidate for your target language. Request more candidates when localization quality for a specific language matters.

#### Request example

```json
{
  "targetLanguage": "de-DE"
}
```

### Response

Status: `200`. Content type: `application/json`.

- `draftVoiceId` (string) — Identifier of the draft holding the candidates. Pass it to [Approve a localization](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/approve-localization.md). Drafts are not listed among your voices and expire after 3 days if never approved.
- `candidates` (object[]) — Localized prompt candidates, one per synthesis engine. Listen to each preview and pick the one that sounds most natural and native.
  - `candidateId` (string) — Identifier for this candidate. Pass it to [Approve a localization](https://docs.inworld.ai/api-reference/voiceAPI/voiceservice/approve-localization.md) together with the `draftVoiceId`. Candidate IDs are replaced if you localize the same voice and language again — always approve from the most recent localize response.
  - `engine` (string) — Opaque label identifying which synthesis engine produced this candidate (e.g. `engine_a`). Labels are stable across responses; candidates from different engines have different strengths, so listen to each.
  - `previewAudio` (string) — Base64-encoded WAV preview of the candidate speaking the target-language text, typically 9-15 seconds.
  - `transcription` (string) — The target-language text this candidate speaks — the automatic translation of the voice's source text, or your `targetTranscription` verbatim when it was supplied.
  - `qualityScore` (number) — Deprecated. Always `0.0` and scheduled for removal — candidates are not automatically scored. Listen to each candidate's `previewAudio` and choose by ear.

### Response examples

#### 200: success

```json
{
  "draftVoiceId": "your_workspace_id__localize-70f3d90e",
  "candidates": [
    {
      "candidateId": "293127bb4b69",
      "engine": "engine_b",
      "previewAudio": "<base64-wav-bytes>",
      "transcription": "Hallo Welt"
    },
    {
      "candidateId": "f81e64399e72",
      "engine": "engine_a",
      "previewAudio": "<base64-wav-bytes>",
      "transcription": "Hallo Welt"
    }
  ]
}
```

#### default: languageCap

```json
{
  "code": 3,
  "message": "This voice has 20 languages and the maximum is 20",
  "details": []
}
```

### Code examples

#### cURL

```bash
curl --location 'https://api.inworld.ai/voices/v1/voices/<voice-id>:localize' \
--header "Authorization: Basic $INWORLD_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
  "targetLanguage": "de-DE"
}'
```

#### Python

```python
import base64
import requests

voice_id = "<voice-id>"
url = f"https://api.inworld.ai/voices/v1/voices/{voice_id}:localize"
headers = {
    "Authorization": "Basic <api-key>",
    "Content-Type": "application/json"
}
payload = {"targetLanguage": "de-DE"}

response = requests.post(url, json=payload, headers=headers)
data = response.json()

# Save each candidate preview to a WAV file so you can listen
for c in data["candidates"]:
    with open(f"candidate_{c['candidateId']}.wav", "wb") as f:
        f.write(base64.b64decode(c["previewAudio"]))
print(data["draftVoiceId"], [c["candidateId"] for c in data["candidates"]])
```

#### JavaScript

```javascript
const voiceId = '<voice-id>';
const url = `https://api.inworld.ai/voices/v1/voices/${voiceId}:localize`;

const response = await fetch(url, {
  method: 'POST',
  headers: {
    'Authorization': 'Basic <api-key>',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ targetLanguage: 'de-DE' }),
});

const data = await response.json();
console.log(data.draftVoiceId, data.candidates.map((c) => c.candidateId));
```
