Build with Realtime TTS
Synthesize Speech (Async)
Submit a synthesis job, poll for completion, and download the results from signed URLs
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 text, the server immediately returns a long-running operation, 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.
For the simplest integration with short text up to 2,000 characters, use the Synthesize Speech API. For real-time playback, use the Streaming API or, for the lowest latency, the WebSocket API. To submit many separate requests at a lower price and collect the results later, use the Batch API.
How it works
Submit the job
POST /tts/v1/voice:synthesizeAsync with the same request body as synchronous synthesis. The response is an operation with a name like workspaces/{workspace}/ttsAsyncJobs/{job}/operations/{operation} and done: false.
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.
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.
# 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)"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()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.
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. |
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.
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:
# 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(orNOT done, ordone=false) returns only running jobs;filter=done(ordone=true) only finished ones.- Results are unordered and cover roughly the last 7 days — operations expire together with their results.
- Paginate with
pageTokenuntil a response has nonextPageToken. 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.
Limits
| Limit | Value |
|---|---|
| Models | inworld-tts-2 only. A request naming any other model is rejected at submit |
| Text per request | 100,000 characters. On-Demand accounts are capped at 10,000 — see Job Concurrency Limits |
| Result retention | ~7 days from completion, after which audioUri and timestampsUri expire |
| Concurrent jobs | Per account, by plan — see Job Concurrency Limits |
For more than 100,000 characters, split the text and submit several jobs, or use the Batch API if the pieces are independent — its per-item ceiling is the same 100,000, but one job carries up to 10,000 items.
API Reference
Synthesize Speech (Async)
Submit an asynchronous synthesis job
Get Async Operation
Poll a job's operation until it completes
List Async Operations
List your jobs across the workspace