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.

5 min readSume
All posts

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 mode and the submit is async: it answers at once, with the job, status_url, and result_url inside data.
  • 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/avatars never returns one, but it lists your avatars, and one whose voice.status is ready works as avatar_id or avatar_handle. A voi_ 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_seconds is the suggested delay before the next poll. The docs say to honor it when present and to back off otherwise.
  • /result answers 409 job_not_completed until result_ready is true. On failed or canceled, read the reason from GET /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 timeout on 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.

From the TTS 1.0 schema in the Sume API reference and current code, read 2026-09-27.
You wantAdd to the bodyIn 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 sentenceAlso "segmentation": {"mode": "sentence"}, with word timings on and WAV outputsegments[], each with its own audio_url
Non-English speech"language": "ko" (a BCP-47 / ISO-639 code) and a transcript in that languageSpeech 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

All Developers posts

Written by Sume