Eight market beds in parallel: asyncio, one idempotency key each
Eight Music Router jobs with one Idempotency-Key per market and a metadata tag cost $1.00 and survive a retry without a second charge. A runnable Python script.

Eight market-specific beds cost 8 x $0.125 = $1.00 on Sume, and you can submit them in parallel from Python with one Idempotency-Key per market, so a retried request does not create a second paid job. The script below uses only the standard library, runs the blocking HTTP calls in threads under asyncio, polls each job with backoff, and prints the job id per market.
The idempotency key is the part that protects the money. The Jobs docs say not to submit the original paid request again only because a local process timed out; with a stable key (bed-20261008-us) a re-run of the same script reuses the key for the same market.
The script
Set SUME_API_KEY first. The script fails at start if the variable is missing. Each market gets a different tempo so the beds differ, and metadata carries the market code. The Music docs say Sume stores metadata on the job and does not send it to the provider.
import asyncio, json, os, urllib.request
KEY = os.environ["SUME_API_KEY"]
MARKETS = ["us", "uk", "de", "fr", "kr", "jp", "br", "mx"]
def call(method, path, body=None, idem=None):
headers = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
if idem:
headers["Idempotency-Key"] = idem
data = json.dumps(body).encode() if body else None
req = urllib.request.Request("https://api.sume.com" + path, data, headers, method=method)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.load(resp)
async def bed(market):
bpm = 92 + 4 * MARKETS.index(market)
prompt = f"Warm pop, {bpm} BPM, A minor. A 59-second track. Instrumental, no vocals."
job = await asyncio.to_thread(call, "POST", "/v1/music-router/generate",
{"prompt": prompt, "metadata": {"market": market}}, f"bed-20261008-{market}")
job_id, delay = job.get("id") or job["request_id"], 2
while (await asyncio.to_thread(call, "GET", f"/v1/jobs/{job_id}/status"))["status"] not in (
"completed", "failed", "canceled"):
await asyncio.sleep(delay)
delay = min(delay * 2, 20)
return market, job_id
async def main():
print(await asyncio.gather(*(bed(m) for m in MARKETS)))
asyncio.run(main())Notes on the shape
The job id is read from id or request_id, since the docs call the submit response a job and use request_id for the job id on some surfaces; read the real response once and keep the one you see. Status values are queued, processing, completed, failed and canceled, and the loop stops on the last three. The result sits at GET /v1/jobs/{id}/result, where the audio artifact has type: "audio" in result.artifacts[].
| Item | Value |
|---|---|
| Beds | 8 market tags |
| Price per bed | $0.125 flat (Music 1.0 and Music Router) |
| Batch total | 8 x $0.125 = $1.00 |
| Tempo spread | 92 to 120 BPM in steps of 4 |
| Retry guard | Idempotency-Key bed-20261008-<market> |
When one market fails
The script prints ids and does not read results, so a failed job is only visible as the last status. In a real pipeline, branch on that status and log the market. Do not resubmit with the same key hoping for a different outcome: the same key and body is the same request. If a brief was flagged, change the flagged content and keep the musical brief, as the Music docs advise, and use a new key such as bed-20261008-us-2.
Budget for it. Read the failed job's public error metadata (category, retryability, next action) before you decide to retry; the queue category, for example, says to retry later with the same idempotency key. In the worst case, three re-tries on two markets add 6 x $0.125 = $0.75, and the batch is $1.75 instead of $1.00.
Limits to expect
The Generation admission page says paid generation jobs can wait as queued, and that when the queue is full, submits fail with 429 queue_full. It lists the accepted-job capacity by plan (Free 6, Pro 24, Startup 48). If you submit eight on a plan with a capacity of 6 and get a 429, wait and run the script again with the same keys; the docs say to retry with backoff and an idempotency key. The page describes this for paid generation jobs, so confirm on your own workspace whether music counts.
After the batch finishes, fetch each result and store the media.sume.com URL, not a provider URL. The Music docs say raw provider URLs are not public outputs.
Sources
Related posts
More in Developers
- Elixir Req: submit and poll a Sume video job after Sora
An Elixir script using Req: POST /v1/videos with an Idempotency-Key, poll every 30 seconds, stop on completed, failed or cancelled. Req never retries the POST.
- Env and secrets diff for removing Sora: SUME_API_KEY, one auth header
Which environment variables to delete, add and rotate when a service leaves the OpenAI Videos API for Sume, plus a startup check that fails on a missing secret.
- Extract 16 kHz mono audio from a video for transcription
A Python script that detaches speech-ready 16 kHz mono wav from a Sume-hosted video, then polls the job. Costs $0.01 per detach before the STT minute.
- Face swap request in Python: validate the video URL before you send
A Python snippet that rejects non-HTTPS, localhost and private-IP video URLs locally, then submits a Sume face-swap beta run with a quality and idempotency key.
Written by Sume