Idempotency-Key from a sha256 of the body: Seedance 2.5 retries
Retry a timed-out POST /v1/videos without paying twice by deriving Idempotency-Key from the request body. Python code plus the 409 conflict rule on Sume.

A deterministic Idempotency-Key is a hash of the exact request body plus a label for your intent, so that a retry after a timeout always sends the same key and Sume returns the original job instead of a second paid one. For POST /v1/videos, send it as the Idempotency-Key header and never reuse it with a different body.
What Sume guarantees
Sume documents Idempotency-Key as supported on POST /v1/videos. The OpenRouter video route has no equivalent, which is a listed difference. A replay returns the original job, and for sume/auto it also resolves to the same route and the same price, because auto resolution is a pure function of the request and the catalog version.
If you reuse a key with a different body, the API answers 409 and does not create a job. The generation admission page names the code idempotency_conflict. That is the safety rail, and it is also why a random key per attempt is the wrong design: a retry would look like a brand-new job.
Think of the key as the receipt for one decision. You decided to render this storyboard beat once, and the key lets every retry, from any worker, prove it is the same decision. Without it the API cannot tell a nervous retry from a deliberate second order, so it has to treat them alike and charge both.
Random keys versus derived keys
There are two sound strategies. Store a UUID with the intent before you send, or derive the key from the content. Derived keys need no storage, which is useful when a worker crashes between building the request and recording it.
| Strategy | Survives a crash | Same body twice | Risk |
|---|---|---|---|
| Fresh uuid4 per attempt | No | Two paid jobs | Double billing on retry |
| uuid4 stored before send | Yes | One job | Needs a write first |
| sha256 of body plus intent label | Yes | One job | Two real intents collide without a label |
| Counter or timestamp | No | Two paid jobs | Drifts on retry |
Code
The label is the part people forget. If you genuinely want two takes of one prompt, change the label, for example take-1 and take-2. Key order matters for the hash, so serialize with sorted keys.
import hashlib, json
def idem_key(body: dict, label: str) -> str:
canon = json.dumps(body, sort_keys=True, separators=(",", ":"))
return label + "-" + hashlib.sha256(canon.encode()).hexdigest()[:32]
body = {"model": "seedance-2.5", "prompt": "Rooftop sunrise, slow orbit",
"duration": 30, "resolution": "1080p", "aspect_ratio": "16:9"}
k1 = idem_key(body, "campaign-42-take-1")
k2 = idem_key(dict(reversed(list(body.items()))), "campaign-42-take-1")
k3 = idem_key(body, "campaign-42-take-2")
assert k1 == k2 # same intent, same key, any dict order
assert k1 != k3 # a second take is a different key
print(k1) # send as the Idempotency-Key header on POST /v1/videos
Where retries happen
The key earns its keep in four places. A client timeout, where the response is lost but the job exists. A 429 rate_limited, where Sume tells you to retry with the same key. A 429 queue_full, where capacity opens later. And 503 provider_capacity_exceeded, where Sume could not dispatch. In each case the docs say to retry with the same key, not a fresh one.
The cost of a wrong guess is real for long clips. Seedance 2.5 takes 4 to 30 seconds per job, so a duplicate 30-second render is a full price duplicate.
A worked example makes the stakes clear. A 30-second clip at 1080p on Seedance 2.5 is as long as a single job gets on the video catalog, and a retry storm after a network blip can resend the same submit three times in a minute. With a derived key, those three requests collapse to one job. With a fresh key each time, you own three renders and pay for three, and you find out on the invoice, not in the logs.
Rules to keep
- One key per intent. Change the body, change the key.
- Keep keys under your own prefix so you can search your logs for a campaign.
- Do not put secrets or personal data in the key, since it appears in logs.
- Also store the returned job id. The key protects the submit, and the id is how you poll.
Sources
Related posts
More in Developers
- Burned-in captions and YouTube AI disclosure: captions are exempt
YouTube's disclosure page names caption creation among edits that need no altered-content label. What that covers when Sume burns captions into a Short.
- Does the previous agent turn help transcription? AssemblyAI says 4.3%
AssemblyAI reports agent context cut entity error from 16.04% to 15.35%, a 4.3% relative drop. Sume STT has no context field; here is a fair test.
- Download a 4K Gemini Omni clip from /content: redirect, 409 and retry
GET /v1/videos/{id}/content answers 302 to the file, or 409 job_not_completed while rendering. Python that reads the redirect without leaking your API key.
- Does Dr. split a Sume STT segment? Abbreviations and sentence mode
Segmentation tests each token for a final period, so a token like Dr. or U.S. can end a segment early. Merge on your side with a small abbreviation list.
Written by Sume