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

# Synthesize Speech (Batch)

> Submit many independent synthesis requests as one job, poll for completion, and download a results file listing every item

<Note>
  **Preview.** This API is a preview release and may be further refined before it is marked stable. See [release stages](/portal/support) for what that means.
</Note>

You submit a list of synthesis requests, the server immediately returns a [long-running operation](https://google.aip.dev/151), and the whole set is synthesized in the background. Poll the operation until it is done, then download a results file that names every item's audio.

Best for large libraries generated in one pass — every line of dialogue in a game, a catalogue of product descriptions, or re-recording an existing script after a voice change. Each item carries its own request, so a single batch can mix voices, models, languages, and audio formats freely.

<Note>
  For one long piece of text, use the [Async API](/tts/synthesize-speech-async) — batch is for many separate requests, not for splitting one. For a single request/response, use the [Synthesize Speech API](/tts/synthesize-speech).
</Note>

## How it works

<Steps>
  <Step title="Submit the batch">
    `POST /tts/v1/voice:synthesizeBatch` with an `items` array. Each item pairs a `customId` of your choosing with a `request` identical in shape to [synchronous synthesis](/api-reference/ttsAPI/texttospeech/synthesize-speech). The response is an operation named `workspaces/{workspace}/ttsBatchJobs/{batch}/operations/{operation}` with `done: false`.
  </Step>

  <Step title="Poll the operation">
    `GET /lro/v1alpha/{name}` with the full operation name (including its slashes) in the URL path — the same endpoint async jobs use. Poll every few seconds; a large batch can take minutes.
  </Step>

  <Step title="Download the results file">
    When `done` is `true`, the `response` carries a `resultsUri` rather than per-item URLs — a batch's results do not fit in one response. Fetch that file to get every item's outcome, then fetch each item's `audioUri`. All of these are pre-signed URLs: request them **without** an `Authorization` header. They expire at `expireTime`, approximately 7 days after completion.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={"system"}
  # 1. Submit — returns an operation name
  OPERATION=$(curl -s 'https://api.inworld.ai/tts/v1/voice:synthesizeBatch' \
    --header "Authorization: Basic $INWORLD_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{
      "items": [
        {
          "customId": "chapter-01",
          "request": {
            "text": "The first chapter begins on a quiet morning.",
            "voiceId": "Ashley",
            "modelId": "inworld-tts-2",
            "audioConfig": { "audioEncoding": "MP3" }
          }
        },
        {
          "customId": "chapter-02",
          "request": {
            "text": "The second chapter opens with a storm at sea.",
            "voiceId": "Dennis",
            "modelId": "inworld-tts-2",
            "audioConfig": { "audioEncoding": "MP3" }
          }
        }
      ]
    }' | jq -r '.name')

  # 2. Poll until done: true
  until curl -s "https://api.inworld.ai/lro/v1alpha/$OPERATION" \
    --header "Authorization: Basic $INWORLD_API_KEY" \
    | tee operation.json | jq -e '.done == true' > /dev/null; do
    sleep 5
  done

  # 3. Download the results file, then each item's audio
  #    (signed URLs — no Authorization header)
  curl -s -o results.json "$(jq -r '.response.resultsUri' operation.json)"
  jq -r '.results[] | select(.audioUri) | "\(.customId) \(.audioUri)"' results.json \
    | while read -r ID URL; do curl -s -o "$ID.mp3" "$URL"; done
  ```

  ```python Python theme={"system"}
  import time

  import requests

  BASE = "https://api.inworld.ai"
  HEADERS = {"Authorization": "Basic <api-key>"}


  def item(custom_id, text, voice_id):
      return {
          "customId": custom_id,
          "request": {
              "text": text,
              "voiceId": voice_id,
              "modelId": "inworld-tts-2",
              "audioConfig": {"audioEncoding": "MP3"},
          },
      }


  # 1. Submit
  operation = requests.post(
      f"{BASE}/tts/v1/voice:synthesizeBatch",
      headers=HEADERS,
      json={
          "items": [
              item("chapter-01", "The first chapter begins on a quiet morning.", "Ashley"),
              item("chapter-02", "The second chapter opens with a storm at sea.", "Dennis"),
          ]
      },
  ).json()

  # 2. Poll
  while not operation.get("done"):
      time.sleep(5)
      operation = requests.get(
          f"{BASE}/lro/v1alpha/{operation['name']}", headers=HEADERS
      ).json()

  if "error" in operation:
      raise RuntimeError(f"Batch failed: {operation['error']['message']}")

  # 3. Download the results file, then each item's audio
  #    (signed URLs — no Authorization header)
  results = requests.get(operation["response"]["resultsUri"]).json()

  # failedItems is omitted rather than sent as 0 when everything succeeded.
  if results.get("failedItems", 0):
      for entry in results["results"]:
          if "error" in entry:
              print(f"{entry['customId']} failed: {entry['error']['message']}")

  for entry in results["results"]:
      if "audioUri" in entry:
          audio = requests.get(entry["audioUri"]).content
          open(f"{entry['customId']}.mp3", "wb").write(audio)
  ```
</CodeGroup>

## Correlating results

`customId` is required, must be unique within the batch, and is the supported way to match a result back to what you submitted. Results also appear in submission order, but prefer the key — it stays correct if you ever submit in a different order than you read.

Treat it as opaque: the service never interprets it, only echoes it back — in the results file, and in any error naming the item it belongs to. Keep it under 64 characters; longer ids are truncated where they are echoed into error messages.

## When items fail

A batch is rejected as a whole, at submit, if **any** item is invalid — an unknown voice, an unsupported model, text past the ceiling. The error names the offending item by both of your handles on it, for example `items[1] (custom_id "chapter-02"): Unknown voice: Nope not found!`. Nothing is synthesized and no job is created, so fix the item and resubmit.

Once a batch is running, the rule inverts: an item that fails during synthesis gets an `error` in its results entry instead of an `audioUri`, and the rest of the batch still completes. Check `failedItems` and the per-item `error` field rather than assuming every entry has audio.

<Note>
  `failedItems` is **omitted** from the results file rather than sent as `0` when every item succeeds, so read it with a default (`results.get("failedItems", 0)`) rather than testing whether the key is present. The same applies to any zero-valued field in these responses.
</Note>

### Items that request timestamps

An item that sets `timestampType` needs alignment to succeed as well as synthesis. If alignment cannot be produced, the item fails outright — batch never returns audio with the timestamps quietly missing, because an item that asked for timing and got none is not the result that was asked for.

Timestamp availability is tracked separately from synthesis, so a language can in principle synthesize while alignment for it is unavailable. **Wherever that is true you find out at submit — never after paying for audio you cannot use due to the lack of timestamps.** Such an item is rejected before any job exists and, like every other invalid item, takes the whole batch with it:

```
INVALID_ARGUMENT: items[1] (custom_id "chapter-02"): timestamps are not
available for language 'zz-ZZ'. Omit timestamp_type to synthesize audio
without them, or use a supported language.
```

No job is created and nothing is charged. The error names both the item and the language, so you do not need to check anything in advance: if a batch is accepted, timestamps are available for every item in it. Fix the named item and resubmit.

An item that passes submit can still fail during synthesis. Two outcomes are worth telling apart, because only one is worth retrying:

| Item `error.code`     | Meaning                                                                                                                                                                                                                                                                      | What to do                                                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `FAILED_PRECONDITION` | Alignment was not possible for this item — reported as `timestamps are not available for the requested language`. Reaches you rather than the submit rejection when the item's language could not be known at submit, most often because it was auto-detected from the text. | Permanent. Resubmit without `timestampType`, or set an explicit supported `language`. Retrying unchanged fails identically. |
| `UNAVAILABLE`         | Alignment was temporarily unreachable.                                                                                                                                                                                                                                       | Transient. Resubmit the item.                                                                                               |

<Note>
  The synchronous and streaming endpoints behave the **opposite** way: they keep the audio and return success with no `timestampInfo` at all. Only async and batch jobs turn a timestamp failure into a failure of the request. If you are porting code from those endpoints, an item error where you previously saw silently missing timestamps is expected.
</Note>

## Limits

| Limit              | Value                                                                                                                                                                                                                                             |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Items per batch    | 1 – 10,000                                                                                                                                                                                                                                        |
| Request size       | 16 MiB total, roughly 4M characters — the item ceiling is only reachable with short items                                                                                                                                                         |
| Text per item      | 100,000 characters                                                                                                                                                                                                                                |
| Text per batch     | Unlimited on most plans, subject to the request size above. On-Demand accounts are capped at 10,000 characters across all items of one batch — see [Job Concurrency Limits](/resources/job-concurrency-limits#per-job-size-on-the-on-demand-plan) |
| Result retention   | \~7 days from completion, after which every URL in the results file expires                                                                                                                                                                       |
| Concurrent batches | Per account, by plan — see [Job Concurrency Limits](/resources/job-concurrency-limits)                                                                                                                                                            |
| Queued characters  | Per account, by plan — the total text across all of your unfinished batches                                                                                                                                                                       |

<Warning>
  Submissions over either per-account limit are rejected with a `RESOURCE_EXHAUSTED` error and create nothing — no operation, and no partial synthesis. Retry once earlier batches finish. Because a batch holds up to 10,000 items, prefer packing work into one batch over submitting many small ones; see [Pack items into one batch](/resources/job-concurrency-limits#pack-items-into-one-batch).
</Warning>

Persist the operation `name` from every submit response. If you lose one, list operations across all of your batch jobs — `GET /lro/v1alpha/ttsBatchJobs/-/operations`, optionally with `filter=-done` for running batches only. See [Listing your jobs](/tts/synthesize-speech-async#listing-your-jobs) for the full contract.

## API Reference

<CardGroup cols={2}>
  <Card title="Synthesize Speech (Batch)" icon="code" href="/api-reference/ttsAPI/texttospeech/synthesize-speech-batch">
    Submit a batch synthesis job
  </Card>

  <Card title="Get Async Operation" icon="code" href="/api-reference/ttsAPI/texttospeech/get-async-operation">
    Poll a job's operation until it completes
  </Card>

  <Card title="List Batch Operations" icon="code" href="/api-reference/ttsAPI/texttospeech/list-batch-operations">
    List your batch jobs across the workspace
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={3}>
  <Card title="Synthesize Speech (Async)" icon="clock" href="/tts/synthesize-speech-async">
    Submit a single long-form job instead of many separate ones.
  </Card>

  <Card title="Timestamps" icon="stopwatch" href="/tts/capabilities/timestamps">
    Get word or character timing alignment alongside your audio.
  </Card>

  <Card title="Speech Generation Best Practices" icon="circle-check" href="/tts/best-practices/generating-speech">
    Learn best practices for synthesizing high-quality speech.
  </Card>
</CardGroup>
