A blank Idempotency-Key on Sume is ignored, not rejected: guard it
An empty or whitespace Idempotency-Key header is treated as no key at all, so a retry can create a second job. Build a key that cannot be blank in Python.

If a Sume submit goes out with Idempotency-Key: and nothing after it, the API does not return an error. The shared header reader trims the value, and an empty result is read as no key. The request is accepted as an ordinary submit, with no replay protection at all. A retry after a timeout then creates a second job, and a second charge.
How a blank key happens
- An environment variable such as
JOB_KEYis unset, and the template renders an empty string. - A key read from a record field that is missing, for example
(row.get("key") or "").strip(). - A no-code tool sends the header with an empty mapped value.
What the API does with the header
The API handles the header in two ways. A missing header or a blank one means no idempotency. A value longer than 255 characters, or one with control characters, returns 400 invalid_request. The valid range is 1 to 255 printable characters, so the blank case is the one that fails silently.
A key that cannot be empty
Derive the key from the content of the work and hash it. The hash is always 64 characters, it is stable across retries, and an empty input still produces a key, so the blank case is impossible. The function refuses a blank label because a label is how you tell two jobs apart.
import hashlib, json
def idempotency_key(namespace, **fields):
"""Same inputs, same key. Different inputs, different key. Never blank."""
if not namespace.strip():
raise ValueError("namespace must not be blank")
payload = json.dumps(fields, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(f"{namespace}\n{payload}".encode()).hexdigest()
k1 = idempotency_key("avatar-batch-2026-10", sku="A-100", variant="red")
k2 = idempotency_key("avatar-batch-2026-10", sku="A-100", variant="red")
k3 = idempotency_key("avatar-batch-2026-10", sku="A-100", variant="")
print(k1 == k2, k1 == k3, len(k1)) # True False 64Check before you send
Assert 1 <= len(key) <= 255 in your HTTP wrapper before every POST that you might retry. The same key with a different body returns 409 idempotency_conflict, so build the key from the fields that define the job. Never reuse one key across different work.
Sources
Related posts
More in Developers
- Parse the Sume error envelope into a Python dataclass and exception
Sume errors share one envelope: code, request_id, retryable, retry_after_seconds, next_action, category and stage. Turn it into a typed Python exception.
- Get the Sume webhook secret with GET /v1/webhooks/signing-secret
Read the workspace webhook secret with an account:read key and load it as SUME_COM_WEBHOOK_SIGNING_SECRET without printing it. A Python deploy step.
- GET /v1/jobs 400 unknown_parameter: a typo'd filter no longer widens
A misspelled query key on GET /v1/jobs, such as state for status, now returns 400 with a suggestion instead of a full unfiltered page. Handle it in TypeScript.
- Sume job status headers: cache-control no-store and x-sume-poll-after
GET /v1/jobs/:id/status is never cacheable and sends x-sume-poll-after: 2. What each header means for CDNs, browsers and a TypeScript poll loop.
Written by Sume