Derive the Idempotency-Key from a SHA-256 of the request in Python
A key built from the path and a canonical body hash makes a retry return the original Sume job and a changed prompt a new one, with no 409 to handle.

Build the Idempotency-Key as a prefix plus the SHA-256 of the request path and a canonical JSON body. A retry then sends the same key and gets the original job, and a changed prompt produces a different key, so you never hit a 409 idempotency_conflict by accident. The Sume docs say to reuse a key only for the same operation and payload, and a content hash enforces that rule in code.
Why a random key per attempt is wrong
A fresh UUID on every call defeats the point. If your client times out and retries with a new UUID, Sume sees two paid requests and bills two jobs. The docs tell you not to resubmit a paid request only because a local worker timed out, and to send the same key when you retry the submit itself. The sync wait is capped at 30 seconds, so many video submits return before the job is done, and a retry on a network error is common.
| Strategy | Retry after timeout | Changed prompt | Risk |
|---|---|---|---|
| New UUID per call | Creates a second paid job | New job | Double billing |
| Fixed key per batch item | Returns the original job | 409 idempotency_conflict | Edits need a new key |
| Hash of path and body | Returns the original job | New key, new job | Re-rolling an identical request needs a salt |
The helper
Sort the keys and strip whitespace so two dicts with the same content hash the same. Include the path so one body sent to two routes gets two keys. Keep a namespace prefix so you can tell your keys apart in logs. The docs do not state how long Sume remembers a key, so treat replay protection as a retry safeguard, not as a permanent registry.
import hashlib, json, os
import httpx
def idem_key(path: str, body: dict, ns: str = "shots-v1", take: int = 0) -> str:
canon = json.dumps(body, sort_keys=True, separators=(",", ":"))
digest = hashlib.sha256(f"{path}\n{canon}\n{take}".encode()).hexdigest()
return f"{ns}-{digest[:32]}"
def submit(path: str, body: dict, take: int = 0) -> dict:
key = os.environ.get("SUME_API_KEY", "")
if not key:
raise SystemExit("set SUME_API_KEY")
r = httpx.post(
"https://api.sume.com" + path,
json=body,
headers={"Authorization": f"Bearer {key}",
"Idempotency-Key": idem_key(path, body, take=take)},
timeout=60,
)
r.raise_for_status()
return r.json()
body = {"prompt": "Product hero shot of a matte black bottle", "mode": "async"}
print(idem_key("/v1/image-1.0/generate", body))Deliberate re-rolls
Sometimes you do want a second job from an identical request, for example a second take. Pass take=1 and the hash changes. Log the key next to the job id so support can match a request to a job. If you ever do receive a 409 idempotency_conflict, it means something other than the body changed your intent, such as a different route behind the same key, so stop and look instead of retrying.
Where retries come from
Retries come from more places than a timeout. A queue worker that crashes after the HTTP call but before it saves the job id will run the task again. A load balancer can replay a request. A user can double-click a button. A hash-derived key handles all three the same way, because the second request is byte-for-byte the first. A random per-call key handles none of them.
The Sume docs list the same advice in several places: send Idempotency-Key on every paid submit that a client can retry, store the job id, and never submit again only because a local worker timed out.
Keep the body canonical
Canonical means the same bytes for the same meaning. Sort keys, drop optional fields that are None before you hash, and normalize numbers so 8 and 8.0 do not differ. If you send a field only sometimes, decide once whether its absence and its default value are the same request. If they are, hash after you fill in defaults. If not, leave them apart.
Finally, keep the hash input free of anything that changes between attempts, such as a timestamp, a request counter or a random nonce. Those turn the scheme back into a random key.
Sources
Related posts
More in Developers
- Detach 16 kHz mono audio from a 25-minute video: 2 ranges, 2 cents
A 25-minute video exceeds audio detach's 900-second output cap, so request two 750-second ranges at 16 kHz mono: two jobs at $0.01 each, about 24 MB per wav.
- detach_source_has_no_audio: check probe.has_audio first
Audio detach fails with detach_source_has_no_audio on silent video. Probe has_audio with a free frames-false video inspect first. Refusal codes listed.
- detach_source_has_no_audio: the free probe that prevents it on Sume
Sume audio detach fails with detach_source_has_no_audio on a video with no sound track. A frames-off video inspect reads probe.has_audio first. Codes inside.
- Deterministic Sume Idempotency-Key: body hash plus a take counter
Hash customer, take number and canonical body into one key. A retry replays the job; a deliberate redo bumps the take. Python, stdlib only.
Written by Sume