Verify a Sume job.completed webhook in Python (stdlib)
A stdlib Python verifier for Sume job webhooks: HMAC SHA-256 over timestamp.raw_body, five-minute tolerance, rotation-safe, and it refuses an empty secret.

To verify a Sume job webhook in Python, compute HMAC SHA-256 of <timestamp>.<raw_body> with your signing secret, prefix it with sume-v1=, and compare it to every entry in the x-sume-webhook-signature header. Use the raw bytes of the body, not re-serialized JSON, and reject an empty secret or a timestamp outside your tolerance window.
What the docs specify
From the Webhooks docs.
| Item | Value |
|---|---|
| Signed text | <timestamp>.<raw_body>, HMAC SHA 256 |
| Headers | x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex> |
| Rotation | One sume-v1= entry per live secret, comma separated; accept if any matches |
| Replay window | Reject outside tolerance; five minutes is a reasonable default |
| Job events | job.completed, job.failed, job.canceled |
The verifier
Stdlib only. hmac.compare_digest avoids timing leaks, and every entry is compared so a rotation does not reveal which secret matched.
import hashlib, hmac, time
def verify(raw_body: bytes, timestamp: str, header: str, secret: str, tol=300):
if not secret:
raise ValueError("signing secret is empty")
try:
ts = int(timestamp)
except ValueError:
return False
if abs(int(time.time()) - ts) > tol:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256)
expected = "sume-v1=" + mac.hexdigest()
ok = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), expected):
ok = True
return okWhere the secret comes from
Read it from the Webhooks tab of the dashboard, or from GET /v1/webhooks/signing-secret with a key that has account:read. Store it in an environment variable such as SUME_COM_WEBHOOK_SIGNING_SECRET, never in source.
After it verifies
Fetch the image through the job result endpoint, since the webhook is a signal. Keep polling as a backup, as the docs advise, and dedupe on the job id because a delivery can repeat.
Sources
Related posts
More in Developers
- Image job still running after 50 seconds: slow or stuck?
Sume's docs say an image still running at 50 seconds is usually stuck, not slow. What to read first, when not to resubmit, and when to cancel.
- Image job statuses for a progress UI: queued is not failed
Five Sume job statuses, two terminal flags, and a mapping to user-facing labels. Plus why a queued image should read as waiting, not as an error.
- Build an image model capability matrix from GET /v1/images/models
A short Python script that reads Sume's image catalog and prints each model's reference-image limit and aspect ratios, so you stop guessing per model.
- Image request timed out: do you pay for it on Sume?
Sume bills image jobs by outcome. A client timeout does not cancel the job, so a finished image can still bill. Use async mode and poll; do not resubmit.
Written by Sume