Text to speech API in Python: request, poll, save the MP3
Call a text to speech API from Python with Requests: POST the text and a voice, poll the job, then save the MP3. A complete script for Sume TTS 1.0.

To use a text to speech API from Python, POST your text and a voice with requests, wait for the job to finish, then download the returned audio file. On Sume that is POST /v1/tts-1.0/generate, then GET /v1/jobs/{id}/status until the job is terminal, then GET /v1/jobs/{id}/result, which links the finished MP3.
Sume's documented SDK is TypeScript (@sume-com/sdk), so from Python you make plain HTTPS calls. Sume facts come from the TTS 1.0 schema in the Sume API reference, the OpenAPI document behind the API reference docs, and the Jobs and results docs; Requests behavior comes from its Quickstart and Advanced Usage pages. All were read on 2026-09-27. The same pattern for video is in text-to-video API in Python.
How do I send the text?
The body needs a transcript of 1–20,000 characters and one voice: a voice.id, or the avatar_id or avatar_handle of an avatar whose voice is ready. Set language for non-English text. Pass the body with json=, which Requests' docs point to when you want the JSON Content-Type header set for you.
- Omit
modeand the submit isasync: it answers at once, with the job,status_url, andresult_urlinsidedata. - Send an
Idempotency-Key. If the POST times out, resend the same body under the same key, and the retry returns the original job instead of billing a second one. - No voice id yet?
GET /v1/avatar-1.0/avatarsnever returns one, but it lists your avatars, and one whosevoice.statusisreadyworks asavatar_idoravatar_handle. Avoi_id comes from the Voices library in the Sume app; see AI voiceover with your own voice.
What does the whole script look like?
Submit, poll, and save in 25 lines. The loop polls before it reads anything else, because a replayed submit returns the original job, which may already be finished. In the current code audio_url is the audio artifact's public URL on Sume's media host, so the download sends no key; stream=True with iter_content writes it to disk in chunks.
import os, time, requests
AUTH = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
body = {"transcript": "Hello from Python.", "voice": {"id": os.environ["SUME_VOICE_ID"]}}
r = requests.post("https://api.sume.com/v1/tts-1.0/generate", json=body, timeout=30,
headers={**AUTH, "Idempotency-Key": "hello-python-v1"})
r.raise_for_status()
job = r.json()["data"]
while True:
s = requests.get(job["status_url"], headers=AUTH, timeout=30)
s.raise_for_status()
status = s.json()["data"]
if status["terminal"]:
break
time.sleep(status["next_poll_after_seconds"] or 2)
if status["sume_status"] != "completed":
raise RuntimeError(f"TTS job ended as {status['sume_status']}")
res = requests.get(job["result_url"], headers=AUTH, timeout=30)
res.raise_for_status()
with requests.get(res.json()["data"]["result"]["audio_url"], stream=True, timeout=30) as audio:
audio.raise_for_status()
with open("speech.mp3", "wb") as f:
for chunk in audio.iter_content(chunk_size=65536):
f.write(chunk)How long should the poll loop wait?
next_poll_after_secondsis the suggested delay before the next poll. The docs say to honor it when present and to back off otherwise./resultanswers409 job_not_completeduntilresult_readyis true. Onfailedorcanceled, read the reason fromGET /v1/jobs/{id}instead.mode: "sync"holds the submit for at most 30 seconds, a limit on the HTTP wait, not on the job. If the job is still running, poll as above. The route does not stream audio.- Put a
timeouton every call: Requests does not time out unless you set one. - Giving up on the client side does not cancel the job; it keeps running and still bills.
How do I get WAV or word timings instead?
Change the body, not the loop: each row is an entry to add to the script's body dict, which json= sends as JSON (Python's True becomes true). Give a changed body a new Idempotency-Key, because reusing a key with a different payload answers 409 idempotency_conflict.
| You want | Add to the body | In the result |
|---|---|---|
| A WAV file | "output_format": {"container": "wav", "encoding": "pcm_s16le", "sample_rate": 44100} | A WAV audio_url; save it as .wav |
| Word timings | "timestamps": {"words": True} | words and duration_seconds |
| One clip per sentence | Also "segmentation": {"mode": "sentence"}, with word timings on and WAV output | segments[], each with its own audio_url |
| Non-English speech | "language": "ko" (a BCP-47 / ISO-639 code) and a transcript in that language | Speech in that language |
What does text to speech from Python cost?
TTS 1.0 costs $0.0475 per 1,000 characters, plus a 5.5% agent fee by default, and spaces and punctuation count. A replay under the same Idempotency-Key returns the original job and is not billed again. Synthesized audio longer than 1,200 seconds fails with tts_duration_exceeded and captures no credits.
Sources
Related posts
More in Developers
- Text to speech with highlighted words: sync word timings
Highlight words as text to speech plays: get word timestamps with the audio, then mark the word whose time range holds the player's current time.
- Webhook URL rejected as invalid? Sume's webhook URL rules
Sume answers 400 invalid_request when a webhook URL is not public HTTPS. The rules for scheme, host, port, and credentials, and the check at delivery.
- Connect Claude Code, Cursor, or Codex to Sume with hosted MCP
Sume's hosted MCP server at mcp.sume.com/mcp lets coding agents generate images, video, audio, and avatars. Setup, OAuth scopes, and spend gates.
- Signed webhooks for Sume video runs: events, retries, verification
Sume sends one HMAC-SHA256 signed POST when a Format, Action, or Agent Completion run completes or fails. Verify the raw body and dedupe on request_id.
Written by Sume