TTS call returned processing in sync mode: poll, do not resubmit
Sume TTS in sync mode waits at most 30 seconds. If the job is not terminal, poll status_url, and retry a submit only with the same Idempotency-Key.

If a Sume TTS request sent with mode: "sync" comes back still queued or processing, nothing is wrong. The wait is capped at 30 seconds and the job keeps running. Take status_url from the response and poll it until the job is terminal; do not submit a new paid job for the same line. If you must retry the submit itself, send the same Idempotency-Key so you get the original job back.
What does sync actually do?
The OpenAPI text for wait_timeout_seconds says it is an integer from 0 to 30 and bounds how long the HTTP request blocks, not how long the job may take. If no terminal update arrives before the timeout, or the API process has no waiter capacity, the response is still 2xx and carries the current job state with polling URLs. Jobs and results adds that the envelope's sync.timed_out is true after a timeout and sync.capacity_exhausted is true when the wait was skipped.
subscribe is the same bounded wait under another name. For new work the docs recommend async or webhook.
| Mode | What the HTTP call does | What you do next |
|---|---|---|
async (default) | Returns 202 with polling URLs | Poll status_url, then fetch result_url |
sync | Waits up to wait_timeout_seconds (max 30) | If not terminal, poll; do not resubmit |
subscribe | Same as sync | Same as sync |
webhook | Returns 202 and stores the callback | Wait for the terminal callback; keep polling as backup |
How do I poll safely?
Read next_poll_after_seconds when it is present and back off otherwise. Stop on completed, failed or canceled, then fetch result_url. The script below submits in sync mode, then polls if the first answer was not final.
import json, os, time, urllib.request
BASE = "https://api.sume.com"
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json", "Idempotency-Key": "narration-001"}
def call(url, body=None):
req = urllib.request.Request(url, data=body and json.dumps(body).encode(), headers=HEAD)
return json.load(urllib.request.urlopen(req))["data"]
env = call(BASE + "/v1/tts-1.0/generate", {"transcript": "A short line.", "avatar_handle": "@your_avatar", "language": "en", "mode": "sync", "wait_timeout_seconds": 30})
while not env["terminal"]:
time.sleep(env.get("next_poll_after_seconds") or 2)
env = call(env["status_url"])
print(env["result_url"])Why must I reuse the idempotency key?
A 2xx from submit means the job exists and paid work is in flight. A second submit with a new key is a second job. With the same key, the retry returns the original job. Choose a key that names the intent, for example a script id and sentence number, so a restart of your own process reuses it.
Sources
Related posts
More in Developers
- Test call audio for voice agents: Sume TTS at 8 kHz mu-law
Generate repeatable phone-quality test utterances for a voice agent with Sume TTS output_format: 8000 Hz, pcm_mulaw. Fields, limits and a runnable script.
- TTS voice.id: a UUID or a voi_ library id? What Sume accepts
Sume TTS voice.id takes a voice UUID or a voi_ library id. Any other shape fails with 400 invalid_voice_id before a job is queued or credits are reserved.
- TTS word timestamps to Timeline slide starts in Python
Call the Sume TTS Router with word timestamps, find the word that opens each slide, and build the Timeline video array of start times in a short Python script.
- TypeScript 7: an exhaustive switch over Sume job statuses
Turn a Sume job record into done, failed, canceled or running with a never check, so a new status breaks the build. Compiled with tsc 7.0.2 in strict mode.
Written by Sume