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.

5 min readSume
All posts

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.

Key strategies compared, as of 2026-10-08
StrategyRetry after timeoutChanged promptRisk
New UUID per callCreates a second paid jobNew jobDouble billing
Fixed key per batch itemReturns the original job409 idempotency_conflictEdits need a new key
Hash of path and bodyReturns the original jobNew key, new jobRe-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

All Developers posts

Written by Sume