Retry a timed-out video submit without paying twice: Idempotency-Key
Your client timed out after submitting a 30-second video job? Retry with the same Idempotency-Key and Sume returns the original job, not a second bill.

Send the same Idempotency-Key header with the same body on the retry. Sume then returns the original job rather than creating and billing a second one. A 30-second clip at 1080p is not cheap, so a retry loop without a key is a way to pay for the same video twice.
The docs say it directly: when a sync wait ends or the network fails, you can retry the submit itself, and you must use the same key so the retry returns the original job.
What counts as the same request
Use the same key only for the same operation and payload. If you reuse a key for a different body, the API answers 409 idempotency_conflict. That error is useful, because it flags a key that is being generated from the wrong thing.
Derive the key from your own business id, not a random value made on each attempt. An order id plus a version number works. A random UUID created inside the retry loop defeats the point.
| Status and code | Meaning | What to do |
|---|---|---|
| 429 rate_limited | Request volume over the limit | Wait for retry-after, then retry with the same key |
| 429 queue_full | Accepted-job capacity used | Wait for a job to finish or cancel one, then retry with the same key |
| 503 provider_capacity_exceeded | Dispatch queue full | Retry later with the same key |
| 409 idempotency_conflict | Same key, different payload | Fix the key, do not retry |
| 402 insufficient_credits | Balance cannot cover the reservation | Upgrade the plan or submit a cheaper request |
A retry wrapper
This wrapper retries only on network errors and the retryable statuses above, and always resends the identical key.
import os, time, requests
URL = "https://api.sume.com/v1/video-router/generate"
def submit(body, key, tries=5):
h = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
"Idempotency-Key": key}
for n in range(tries):
try:
r = requests.post(URL, headers=h, json=body, timeout=30)
except requests.RequestException:
time.sleep(2 ** n)
continue
if r.status_code in (429, 503):
time.sleep(float(r.headers.get("retry-after", 2 ** n)))
continue
r.raise_for_status()
return r.json()["data"]["request_id"]
raise RuntimeError("submit failed after retries")
print(submit({"model": "wan-3.0", "prompt": "Rain on a neon street",
"duration": 30, "resolution": "480p"}, "order-1042-v1"))Store the job id as soon as you have it
The key protects the submit. The job id protects everything after it. Write the id to your database before you start waiting, so a restarted process picks the job up with GET /v1/jobs/{id}/status and never reaches the submit again.
Sources
Related posts
More in Developers
- Save a finished Seedance clip to your own storage, not the result URL
When a Seedance 2.5 or Wan 3.0 job completes, read /v1/jobs/{id}/result, take the artifact url and content_type, and copy the file into your own storage.
- Save a sidecar JSON with every Sume mp4: job id, model, cost, prompt
Export finished video jobs with a Python script that downloads each mp4 and writes a sidecar JSON with job id, model, cost and the prompt you saved.
- Scan your repo for retiring Google image and TTS ids (Python)
A 30-line Python scanner that finds Imagen 4, Nano Banana and Gemini TTS preview ids in your files and prints the replacement Google names for each one.
- Seedance 2.5 30-second clip via API: the request, poll and price
Send model seedance-2.5 with duration 30 to POST /v1/videos on Sume, poll the job, and expect about $17 at 720p 16:9. Request and Python poll loop.
Written by Sume