Sume music job in Python: 30-second wait, then poll, one paid job
Submit a Lyria track to Sume Music Router with a 30-second sync wait, fall back to polling on timeout, and reuse one Idempotency-Key. Runnable Python.

Submit with mode: sync and wait_timeout_seconds: 30, check result_ready on the response, and if it is false keep polling status_url with the same job. Never post a second request with a new key: a song takes longer than 30 seconds to render often enough that a retry would buy a second $0.125 generation. The script below handles both paths.
How the sync wait works
On the Music Router, mode is async, sync, subscribe or webhook. sync and subscribe are the same bounded wait. wait_timeout_seconds runs 0 to 30. If the job is still queued or the waiter budget is full, the response is still 2xx and carries sync.timed_out or sync.capacity_exhausted. That limit bounds the HTTP wait, not the job.
| Mode | Returns | Use when |
|---|---|---|
| async | Job id and poll URLs at once | Default for new work |
| sync / subscribe | Waits up to 30 s, then current state | A short script that may finish fast |
| webhook | Job id, signed terminal callback later | A server that should not poll |
The script
Set SUME_API_KEY. The same Idempotency-Key makes an accidental double run return the existing job.
import asyncio, json, os, urllib.request
API = "https://api.sume.com"
KEY = os.environ["SUME_API_KEY"]
def call(method, url, body=None, idem=None):
h = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
if idem:
h["Idempotency-Key"] = idem
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, headers=h, method=method)
with urllib.request.urlopen(req) as r:
return json.load(r)
async def main():
body = {"model": "lyria-3.5", "mode": "sync", "wait_timeout_seconds": 30,
"prompt": "Warm lo-fi hip hop, 84 BPM, C minor. A 30-second track. Instrumental, no vocals."}
out = await asyncio.to_thread(call, "POST", f"{API}/v1/music-router/generate", body, "lofi-bed-001")
d = out.get("data", out)
while not d.get("result_ready"):
await asyncio.sleep(d.get("next_poll_after_seconds") or 2)
d = (await asyncio.to_thread(call, "GET", d["status_url"])).get("data", d)
res = await asyncio.to_thread(call, "GET", d["result_url"])
res = res.get("data", res)
res = res.get("result", res)
for a in res.get("artifacts", []):
if a.get("type") == "audio":
print(a["url"])
asyncio.run(main())Cost and retries
Music 1.0 documents a fixed $0.125 per accepted generation, independent of prompt length. Google lists Lyria 3.5 at $0.08 per full song on its own API (read 2026-10-07); Sume charges its own fixed Music price per accepted generation. A failed submit with the same key is safe to retry. A new key for a new take is a new charge by design.
- Read the audio from
result.artifacts[]wheretypeisaudio; the URL is onmedia.sume.com. - Do not send
duration_secondsto Music; the field is rejected. Put the length in the prompt. - Use a distinct, meaningful key per intended take, such as scene and take number.
Sources
Related posts
More in Developers
- Why the Sume catalog shows $0.02 to $42.86 for one video route
GET /v1/catalog publishes an estimate plus a minimum and maximum for each route. Video spans $0.02 to $42.86, image $0.01 to $7.36, TTS $0.01 to $0.95.
- Sume status vocab: a job is completed, a resource is ready
Sume lists three status vocabularies: job, resource, webhook delivery. Only jobs say completed; resources say ready, so a check on the wrong one never matches.
- Sume run webhook: two request_ids, which one to dedupe on
Dedupe a Sume run webhook on the envelope request_id, which equals run_id and is stable across retries. Ignore payload.request_id, a correlation id.
- Voice note to SRT in Python with Sume STT sentence segments
Submit a voice note to Sume STT with sentence segmentation, poll the job, and write an SRT file from segments[]. Runnable Python with asyncio.run.
Written by Sume