Does your Format webhook receiver pass OWASP's checklist?
Check a Sume Format webhook receiver against OWASP's webhook cheat sheet: raw body, HMAC, five-minute window, dedupe on request_id, and where docs are silent.

A Sume Format webhook receiver passes most of OWASP's checklist if it does five things: verify an HMAC-SHA256 signature over the raw body, reject timestamps more than five minutes off, compare in constant time, dedupe on request_id, and answer 2xx before it does the work. Two items are on you or are not documented: keeping event ids long enough, and rotating the signing secret.
The security baseline comes from the Webhook Security Cheat Sheet (read 2026-10-10). Sume's side comes from Runs and results and the Cookbook.
Line by line
The table pairs each OWASP recommendation with what the Sume docs say for a format.run.terminal delivery. Where the docs I read are silent, the row says so instead of guessing.
| OWASP recommendation | Sume Format webhook | Your action |
|---|---|---|
| Sign with HMAC-SHA256 | Signature is HMAC-SHA256 over <timestamp>.<raw_body>, sent as sume-v1=<hex> | Verify before parsing |
| Use the raw request body | Sume signs the raw bytes; a parsed and re-serialized body will not match | Read bytes first |
| Constant-time compare | Not stated by Sume; the Python cookbook receiver uses hmac.compare_digest | Never use == |
| Reject old timestamps (5 minutes is Stripe's library default per OWASP) | Docs: reject timestamps outside a five-minute window | Same window |
| Cache event ids for at least twice the tolerance | request_id is stable across retries and is the dedupe key | Keep ids far longer than 10 minutes |
| Rotate with two signatures | The pages I read describe a secret fingerprint header, not dual signing | Check the fingerprint on every call |
| Outbound: HTTPS only, no private addresses | Sume accepts public HTTPS only and re-validates the URL at delivery | Register your final URL |
Where Sume goes beyond the checklist
Each delivery carries x-sume-webhook-secret-fingerprint, twelve hex characters. Compare it with the fingerprint shown next to your secret and you learn immediately that both sides hold the same secret, which is the most common cause of a signature that never matches. Sume does not follow redirects, treats a 3xx as a failed attempt, and waits up to ten seconds per attempt.
Deliveries also have a dashboard test: POST /v1/webhooks/test-deliveries sends a webhook.test payload, and POST /v1/format-runs/{run_id}/webhook/redeliver replays a real terminal event. Neither uses up the ten automatic attempts.
Where you must supply the control
OWASP says to keep event ids for at least twice the timestamp tolerance, which is ten minutes for a five-minute window. A Sume delivery can be retried up to ten times, with exponential backoff capped at one hour, so a duplicate can arrive hours after the first. Keep request_id for at least a day, and make the insert-or-ignore atomic.
Return 2xx for a duplicate you have already queued, as OWASP advises, and do not repeat its side effects. One more Sume-specific trap: when a receipt is over 1 MiB the envelope arrives with payload: null and an error.result_url, so a handler that always reads payload as an object will fail on your largest runs.
A verifier that refuses an empty secret
This stdlib function follows the documented scheme. It raises on an empty secret instead of quietly accepting every signature, and the demo at the bottom shows a valid body passing and a one-byte change failing.
import hashlib, hmac, os, time
def verify(raw: bytes, ts: str, sig: str, secret: str, now: float | None = None) -> bool:
if not secret:
raise ValueError("empty signing secret")
if not ts.isdigit() or abs((now or time.time()) - int(ts)) > 300:
return False
digest = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest("sume-v1=" + digest, sig)
secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET") or "demo-secret"
raw = b'{"event":"format.run.terminal"}'
ts = str(int(time.time()))
good = "sume-v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
print(verify(raw, ts, good, secret), verify(raw + b" ", ts, good, secret))
try:
verify(raw, ts, good, "")
except ValueError as err:
print("refused:", err)Sources
Related posts
More in Formats
- Which failed Format runs can you retry without paying twice?
A decision table for failed Sume Format runs: which codes mean retry, which mean continue with previous_run_id, and which mean fix your input first.
- Which Format version ran my API call? Check the receipt
Every Sume Format run receipt carries format.version. Read it after you edit a package, so a bulk batch or schedule is never judged against the wrong version.
- Ready-made Formats for product video: the Sume Format catalog
Sume ships ready-made Formats for product and UGC-style video and images, each callable from your backend with one HTTP request at the reserved sume handle.
- What is a Sume Format? Turn an agent thread into one API call
A Sume Format is a saved video recipe your backend calls by handle and slug. One POST runs it in a fresh sandbox and returns media plus optional typed JSON.
Written by Sume