Realtime TTS-2 is live. Built for realtime conversation that feels human. Learn more

Build with Realtime TTS

Synthesize Speech (Batch)

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

Preview. This API is a preview release and may be further refined before it is marked stable. See release stages for what that means.

You submit a list of synthesis requests, the server immediately returns a long-running operation, 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, languages, and audio formats freely.

Batch trades latency for a lower price, on purpose. Characters synthesized through batch are billed at 20% less per character than the same characters through the realtime endpoints. The discount applies on every plan, on top of whatever tier rate you already have, so it does not shrink as you scale up. See the Pricing Page for current rates.

What you give up is time. A batch synthesizes its items a group at a time rather than all at once, so a large one runs for hours, not minutes — and a batch waiting behind your other jobs has not started at all yet. Size the expectation accordingly: submit work you will collect later, not work someone is waiting on. If you need a result now, the synchronous and streaming endpoints are the ones to reach for, at the realtime rate.

Plan for a batch to come back later rather than soon: it is queued against your account's capacity and worked through in the background. We are working toward 24-hour delivery — see Turnaround.

For one long piece of text, use the Async API — batch is for many separate requests, not for splitting one. For the simplest integration with short text up to 2,000 characters, use the Synthesize Speech API.

How it works

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. The response is an operation named workspaces/{workspace}/ttsBatchJobs/{batch}/operations/{operation} with done: false.

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 hours. Each poll also returns a metadata object counting the items finished so far — see Tracking progress.

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.

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

Tracking progress

While a batch is running, its operation carries a metadata object counting the items it has finished. A batch can occupy the queue for a long time, and done: false alone cannot tell you whether it is waiting behind your other jobs, working through items, or stuck — the counters can.

json
{
  "name": "workspaces/my-workspace/ttsBatchJobs/8f3c.../operations/1a2b...",
  "done": false,
  "metadata": {
    "@type": "type.googleapis.com/ai.inworld.tts.v1.SynthesizeSpeechBatchMetadata",
    "totalItems": 120,
    "completedItems": 50,
    "failedItems": 2,
    "createTime": "2026-08-21T10:04:11Z"
  }
}
FieldMeaning
totalItemsItems in the batch. Fixed when the batch is accepted, so it is readable before any work starts.
completedItemsItems synthesized successfully so far.
failedItemsItems that finished with an error so far. These do not fail the batch — see When items fail.
createTimeWhen the batch was accepted.

Everything not yet counted is totalItems - completedItems - failedItems. That remainder mixes items being synthesized right now with items not started, and the counters cannot tell them apart.

The counters move in steps, not one item at a time. Items are synthesized in groups, and the counts advance a whole group at once — so a 120-item batch reports 0, then 50, then 100, then 120, rather than climbing smoothly. Use them for coarse progress and a stall signal, not for a per-item progress bar. An item's own outcome is only in the results file, after the batch is done.

The counters are also on the terminal operation, so a completed or failed batch still says how far it got. Reading progress is the same GET you already poll with — no extra request:

python
while not operation.get("done"):
    time.sleep(5)
    operation = requests.get(
        f"{BASE}/lro/v1alpha/{operation['name']}", headers=HEADERS
    ).json()
    # Zero-valued fields are omitted, so read every counter with a default.
    progress = operation.get("metadata", {})
    done_so_far = progress.get("completedItems", 0) + progress.get("failedItems", 0)
    print(f"{done_so_far}/{progress.get('totalItems', 0)} items")

A zero counter may be missing rather than 0, depending on which response you are reading. The submit response spells zeros out ("completedItems": 0); the poll response omits them, so a batch with no failures has no failedItems key at all. Read every counter with a default — progress.get("completedItems", 0), as above — and the difference stops mattering. The same rule applies to failedItems in the results file.

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.

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.

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.codeMeaningWhat to do
FAILED_PRECONDITIONAlignment 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.
UNAVAILABLEAlignment was temporarily unreachable.Transient. Resubmit the item.

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.

Limits

LimitValue
Modelsinworld-tts-2 only. An item naming any other model is rejected at submit, and takes the whole batch with it
Items per batch1 – 10,000
Request size16 MiB total, roughly 4M characters — the item ceiling is only reachable with short items
Text per item100,000 characters
Text per batchBounded by the request size above and by your queued-character budget — see below. On-Demand accounts are capped at 10,000 characters across all items of one batch — see Job Concurrency Limits
Result retention~7 days from completion, after which every URL in the results file expires
Concurrent batchesPer account, by plan — see Job Concurrency Limits
Queued charactersPer account, by plan — the total text across all of your unfinished batches

How much text fits in one batch

There is no single "text per batch" number. Two things bound it, and which one binds depends on your plan:

  1. The request size — 16 MiB, roughly 4M characters.
  2. Your queued-character budget — shared across all of your unfinished batches, and a single batch has to fit inside it on its own. A batch bigger than the whole budget is rejected at submit. See Queued characters.

Both are checked at submit, so an oversized batch is refused immediately rather than failing later.

Size is worth thinking about for a second reason: a batch is scheduled work, and a larger one takes proportionally longer to come back — it waits for capacity alongside your other jobs and then works through its items. Where the work divides naturally, several batches finish sooner than one large one, at the same per-character rate, because they are scheduled independently.

Turnaround

We are working toward delivering every batch within 24 hours of acceptance. That is a target we are actively optimizing for rather than a guarantee today, and a very large batch can take longer.

If a batch of yours has not completed within 24 hours, contact support@inworld.ai — we would like to hear about it. Your job is not cancelled while you wait: keep polling the operation and it will complete.

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 — up to the sizing guidance above; see Pack items into one batch.

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 for the full contract.

API Reference

Next Steps