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

# Synthesize Speech (Async)

> Submit a synthesis job, poll for completion, and download the results from signed URLs

<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 text, the server immediately returns a [long-running operation](https://google.aip.dev/151), and synthesis runs in the background. Poll the operation until it is done, then download the finished audio (and optional timestamps) from time-limited signed URLs.

Best for long-form content — audiobooks, podcasts, video voiceovers — or any batch pipeline where you don't want to hold an HTTP connection open for the duration of synthesis.

<Note>
  For a single request/response, use the [Synthesize Speech API](/tts/synthesize-speech). For real-time playback, use the [Streaming API](/tts/synthesize-speech-streaming) or [WebSocket API](/tts/synthesize-speech-websocket). To submit many separate requests as one job, use the [Batch API](/tts/synthesize-speech-batch).
</Note>

## How it works

<Steps>
  <Step title="Submit the job">
    `POST /tts/v1/voice:synthesizeAsync` with the same request body as [synchronous synthesis](/api-reference/ttsAPI/texttospeech/synthesize-speech). The response is an operation with a `name` like `workspaces/{workspace}/ttsAsyncJobs/{job}/operations/{operation}` and `done: false`.
  </Step>

  <Step title="Poll the operation">
    `GET /lro/v1alpha/{name}` with the full operation name (including its slashes) in the URL path. Poll at a modest interval — every few seconds is plenty. Short inputs typically finish within seconds; long inputs can take minutes.
  </Step>

  <Step title="Download the results">
    When `done` is `true`, a successful operation carries a `response`, while a failed one carries an `error` status instead. The `response` contains `audioUri` and (if `timestampType` was requested) `timestampsUri`. These are pre-signed URLs — fetch them **without** an `Authorization` header. They expire at `expireTime`, approximately 7 days after completion, so download results you want to keep.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={"system"}
  # 1. Submit — returns an operation name
  OPERATION=$(curl -s 'https://api.inworld.ai/tts/v1/voice:synthesizeAsync' \
    --header "Authorization: Basic $INWORLD_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{
      "text": "Hello, world! What a wonderful day to be a text-to-speech model!",
      "voiceId": "Dennis",
      "modelId": "inworld-tts-2",
      "audioConfig": { "audioEncoding": "MP3" },
      "timestampType": "WORD"
    }' | 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 (signed URLs — no Authorization header)
  curl -s -o output.mp3 "$(jq -r '.response.audioUri' operation.json)"
  curl -s -o timestamps.json "$(jq -r '.response.timestampsUri' operation.json)"
  ```

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

  import requests

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

  # 1. Submit
  operation = requests.post(
      f"{BASE}/tts/v1/voice:synthesizeAsync",
      headers=HEADERS,
      json={
          "text": "Hello, world! What a wonderful day to be a text-to-speech model!",
          "voiceId": "Dennis",
          "modelId": "inworld-tts-2",
          "audioConfig": {"audioEncoding": "MP3"},
          "timestampType": "WORD",
      },
  ).json()

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

  # 3. Download (signed URLs — no Authorization header)
  if "error" in operation:
      raise RuntimeError(f"Synthesis failed: {operation['error']['message']}")

  result = operation["response"]
  open("output.mp3", "wb").write(requests.get(result["audioUri"]).content)
  if "timestampsUri" in result:  # present only when timestampType was requested
      timestamps = requests.get(result["timestampsUri"]).json()
  ```
</CodeGroup>

<Warning>
  The number of async jobs that can run concurrently is limited per account. Submissions over the limit are rejected with a `RESOURCE_EXHAUSTED` error and create nothing — retry once earlier jobs finish. See [Job Concurrency Limits](/resources/job-concurrency-limits).
</Warning>

## When a job requests timestamps

A job that sets `timestampType` needs alignment to succeed as well as synthesis. If alignment cannot be produced, the whole operation fails and carries an `error` — async never returns an `audioUri` with `timestampsUri` quietly missing, because a job 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.** The request is rejected before any job exists:

```
INVALID_ARGUMENT: timestamps are not available for language 'zz-ZZ'.
Omit timestamp_type to synthesize audio without them, or use a supported language.
```

Nothing is created, nothing is charged, and no concurrency slot is used. The error names the language, so you do not need to check anything in advance: if a job is accepted, timestamps are available for it. Drop `timestampType` if you want the audio without timing.

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

| Operation `error.code` | Meaning                                                                                                                                                                                                                                                                  | What to do                                                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `FAILED_PRECONDITION`  | Alignment was not possible for this request — reported as `timestamps are not available for the requested language`. Reaches you rather than the submit rejection when the 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 job.                                                                                                |

<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, a failed operation where you previously saw silently missing timestamps is expected.
</Note>

## Listing your jobs

Persist the operation `name` from every submit response — it is the primary handle for polling. If you do lose one (a crash between submit and saving the name, a redeploy), list operations across all of your jobs with `-` in place of the job id:

```bash theme={"system"}
# Running async jobs only; drop the filter to include finished ones
curl -s "https://api.inworld.ai/lro/v1alpha/ttsAsyncJobs/-/operations?filter=-done" \
  --header "Authorization: Basic $INWORLD_API_KEY"
```

The workspace is resolved from your API key, so you never have to supply a workspace id. A few things to know:

* `filter=-done` (or `NOT done`, or `done=false`) returns only running jobs; `filter=done` (or `done=true`) only finished ones.
* Results are unordered and cover roughly the last 7 days — operations expire together with their results.
* Paginate with `pageToken` until a response has **no** `nextPageToken`. A short or even empty page can still be followed by more results, so the absent token — not page size — is the end signal.

The same shape works for batch jobs via `ttsBatchJobs/-`. The fully qualified form — `workspaces/{workspace}/ttsAsyncJobs/-/operations`, with the workspace id taken from the first path segment of any operation `name` — is also accepted, and is what `Operation.name` always carries.

## API Reference

<CardGroup cols={2}>
  <Card title="Synthesize Speech (Async)" icon="code" href="/api-reference/ttsAPI/texttospeech/synthesize-speech-async">
    Submit an asynchronous 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 Async Operations" icon="code" href="/api-reference/ttsAPI/texttospeech/list-async-operations">
    List your jobs across the workspace
  </Card>
</CardGroup>

## Next Steps

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

  <Card title="Long Text Input" icon="align-left" href="/tts/capabilities/long-text-input">
    Learn how long inputs are handled across the TTS APIs.
  </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>
