> ## Documentation Index
> Fetch the complete documentation index at: https://dev.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](/api-reference/voiceAPI/voiceservice/approve-localization). Unapproved drafts expire after 3 days.

<Tip>
  **Short URL Path:** `/workspaces/{workspace}` is no longer required in the path for simplicity and clarity. When omitted, the workspace is derived from your API key. The previous URL with the full path `/voices/v1/workspaces/{workspace}/voices/{voice}:localize` would continue to be supported.
</Tip>

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

1. **Localize** (this endpoint) — generates localized prompt candidates and parks them on a hidden draft.
2. **[Approve](/api-reference/voiceAPI/voiceservice/approve-localization)** — 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](/tts/capabilities/multilingual) 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>


## OpenAPI

````yaml post /voices/v1/voices/{voiceId}:localize
openapi: 3.0.0
info:
  title: Inworld Text-to-Speech API
  version: v1
  contact:
    name: Inworld AI
    url: https://inworld.ai
    email: support@inworld.ai
servers:
  - url: https://api.inworld.ai
security:
  - inworld_basic: []
tags:
  - name: VoiceService
paths:
  /voices/v1/voices/{voiceId}:localize:
    post:
      tags:
        - VoiceService
      summary: Localize a voice
      description: >-
        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](/api-reference/voiceAPI/voiceservice/approve-localization).
        Unapproved drafts expire after 3 days.
      operationId: VoiceService_LocalizeVoice
      parameters:
        - name: voiceId
          description: >-
            Voice ID of the voice to localize. Expected format:
            `{workspace}__{voice}`.
          in: path
          required: true
          schema:
            type: string
            pattern: '[^/]+'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VoiceServiceLocalizeVoiceBody'
            example:
              targetLanguage: de-DE
        required: true
      responses:
        '200':
          description: A successful response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/v1LocalizeVoiceResponse'
              examples:
                success:
                  summary: Two localized candidates on a draft
                  value:
                    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:
          description: An unexpected error response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/rpcStatus'
              examples:
                languageCap:
                  summary: Voice is at its language limit
                  value:
                    code: 3
                    message: This voice has 20 languages and the maximum is 20
                    details: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >-
            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"
            }'
        - lang: python
          label: Python
          source: >-
            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"]])
        - lang: javascript
          label: JavaScript
          source: >-
            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));
components:
  schemas:
    VoiceServiceLocalizeVoiceBody:
      type: object
      properties:
        targetLanguage:
          type: string
          description: >-
            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](/tts/capabilities/multilingual) for the
            supported catalog.
        targetTranscription:
          type: string
          description: >-
            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:
          type: integer
          format: int32
          description: >-
            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, and
            the strongest engine for any one language can differ from it — so a
            low `candidateCount` gives you the historically best-performing
            engine rather than the best candidate for your particular target
            language. Request more candidates when localization quality for a
            specific language matters.
      description: Request message for LocalizeVoice custom method.
      required:
        - targetLanguage
    v1LocalizeVoiceResponse:
      type: object
      properties:
        draftVoiceId:
          type: string
          description: >-
            Identifier of the draft holding the candidates. Pass it to [Approve
            a
            localization](/api-reference/voiceAPI/voiceservice/approve-localization).
            Drafts are not listed among your voices and expire after 3 days if
            never approved.
        candidates:
          type: array
          items:
            $ref: '#/components/schemas/v1LocalizedCandidate'
          description: >-
            Localized prompt candidates, one per synthesis engine. Listen to
            each preview and pick the one that sounds most natural and native.
      description: Response message for LocalizeVoice.
    rpcStatus:
      type: object
      properties:
        code:
          type: integer
          format: int32
          description: >-
            The status code, which should be an enum value of
            [google.rpc.Code][google.rpc.Code].
        message:
          type: string
          description: >-
            A developer-facing error message, which should be in English. Any
            user-facing error message should be localized and sent in the
            [google.rpc.Status.details][google.rpc.Status.details] field, or
            localized by the client.
        details:
          type: array
          items:
            $ref: '#/components/schemas/protobufAny'
          description: >-
            A list of messages that carry the error details. There is a common
            set of message types for APIs to use.
      description: >-
        The `Status` type defines a logical error model that is suitable for

        different programming environments, including REST APIs and RPC APIs. It
        is

        used by [gRPC](https://github.com/grpc). Each `Status` message contains

        three pieces of data: error code, error message, and error details.


        You can find out more about this error model and how to work with it in
        the

        [API Design Guide](https://cloud.google.com/apis/design/errors).
    v1LocalizedCandidate:
      type: object
      properties:
        candidateId:
          type: string
          description: >-
            Identifier for this candidate. Pass it to [Approve a
            localization](/api-reference/voiceAPI/voiceservice/approve-localization)
            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:
          type: string
          description: >-
            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:
          type: string
          format: byte
          description: >-
            Base64-encoded WAV preview of the candidate speaking the
            target-language text, typically 9-15 seconds.
        transcription:
          type: string
          description: >-
            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:
          type: number
          deprecated: true
          description: >-
            Deprecated. Always `0.0` and scheduled for removal — candidates are
            not automatically scored. Listen to each candidate's `previewAudio`
            and choose by ear.
      description: One localized prompt candidate.
    protobufAny:
      type: object
      properties:
        '@type':
          type: string
          description: >-
            A URL/resource name that uniquely identifies the type of the
            serialized

            protocol buffer message. This string must contain at least

            one "/" character. The last segment of the URL's path must represent

            the fully qualified name of the type (as in

            `path/google.protobuf.Duration`). The name should be in a canonical
            form

            (e.g., leading "." is not accepted).


            In practice, teams usually precompile into the binary all types that
            they

            expect it to use in the context of Any. However, for URLs which use
            the

            scheme `http`, `https`, or no scheme, one can optionally set up a
            type

            server that maps type URLs to message definitions as follows:


            * If no scheme is provided, `https` is assumed.

            * An HTTP GET on the URL must yield a [google.protobuf.Type][]
              value in binary format, or produce an error.
            * Applications are allowed to cache lookup results based on the
              URL, or have them precompiled into a binary to avoid any
              lookup. Therefore, binary compatibility needs to be preserved
              on changes to types. (Use versioned type names to manage
              breaking changes.)

            Note: this functionality is not currently available in the official

            protobuf release, and it is not used for type URLs beginning with

            type.googleapis.com. As of May 2023, there are no widely used type
            server

            implementations and no plans to implement one.


            Schemes other than `http`, `https` (or the empty scheme) might be

            used with implementation specific semantics.
      additionalProperties: {}
      description: >-
        `Any` contains an arbitrary serialized protocol buffer message along
        with a

        URL that describes the type of the serialized message.


        Protobuf library provides support to pack/unpack Any values in the form

        of utility functions or additional generated methods of the Any type.


        Example 1: Pack and unpack a message in C++.

            Foo foo = ...;
            Any any;
            any.PackFrom(foo);
            ...
            if (any.UnpackTo(&foo)) {
              ...
            }

        Example 2: Pack and unpack a message in Java.

            Foo foo = ...;
            Any any = Any.pack(foo);
            ...
            if (any.is(Foo.class)) {
              foo = any.unpack(Foo.class);
            }
            // or ...
            if (any.isSameTypeAs(Foo.getDefaultInstance())) {
              foo = any.unpack(Foo.getDefaultInstance());
            }

         Example 3: Pack and unpack a message in Python.

            foo = Foo(...)
            any = Any()
            any.Pack(foo)
            ...
            if any.Is(Foo.DESCRIPTOR):
              any.Unpack(foo)
              ...

         Example 4: Pack and unpack a message in Go

             foo := &pb.Foo{...}
             any, err := anypb.New(foo)
             if err != nil {
               ...
             }
             ...
             foo := &pb.Foo{}
             if err := any.UnmarshalTo(foo); err != nil {
               ...
             }

        The pack methods provided by protobuf library will by default use

        'type.googleapis.com/full.type.name' as the type URL and the unpack

        methods only use the fully qualified type name after the last '/'

        in the type URL, for example "foo.bar.com/x/y.z" will yield type

        name "y.z".


        JSON

        ====

        The JSON representation of an `Any` value uses the regular

        representation of the deserialized, embedded message, with an

        additional field `@type` which contains the type URL. Example:

            package google.profile;
            message Person {
              string first_name = 1;
              string last_name = 2;
            }

            {
              "@type": "type.googleapis.com/google.profile.Person",
              "firstName": <string>,
              "lastName": <string>
            }

        If the embedded message type is well-known and has a custom JSON

        representation, that representation will be embedded adding a field

        `value` which holds the custom JSON in addition to the `@type`

        field. Example (for message [google.protobuf.Duration][]):

            {
              "@type": "type.googleapis.com/google.protobuf.Duration",
              "value": "1.212s"
            }
  securitySchemes:
    inworld_basic:
      type: apiKey
      in: header
      name: Authorization
      description: >-
        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](../../../tts/resources/inworld-cli): `inworld workspace add-key`.

````