Verify a Sume image webhook in Python, then read artifacts[]
A 23-line Python verifier for Sume's x-sume-webhook-signature header on an image job: refuses an empty secret, checks a 5-minute window, reads the image URL.

To verify a Sume webhook for an image job in Python, compute an HMAC SHA 256 over the timestamp, a dot and the raw request body, compare it with each sume-v1= entry in x-sume-webhook-signature, and reject deliveries outside a replay window. Refuse to run at all if the secret is empty. The code below does that in 23 lines and prints a pass, a fail and a refusal when you run it.
What Sume sends
Sume's webhook docs say a signed delivery has two headers, x-sume-webhook-timestamp and x-sume-webhook-signature, and that the signature is HMAC SHA 256 over <timestamp>.<raw_body>. During a secret rotation the signature header holds one sume-v1= entry per live secret, newest first, separated by commas. Accept the delivery when any entry matches. The docs suggest five minutes as the replay tolerance.
| Item | Value |
|---|---|
| Events | job.completed, job.failed, job.canceled (terminal only) |
| Timestamp header | x-sume-webhook-timestamp |
| Signature header | x-sume-webhook-signature: sume-v1=<hex>[,sume-v1=<hex>] |
| Signed string | <timestamp>.<raw_body> |
| Replay window | Five minutes is a reasonable default |
| Secret | SUME_COM_WEBHOOK_SIGNING_SECRET, from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret |
The verifier
Verify the raw bytes, not a re-serialized JSON object. If your framework parses the body first, read the raw body before parsing. The comparison uses hmac.compare_digest so it does not leak timing.
import hashlib, hmac, time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret:
raise ValueError("empty signing secret")
if abs(time.time() - int(ts)) > tol:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
parts = [p.strip() for p in header.split(",")]
return any(
hmac.compare_digest(p[len("sume-v1="):], mac)
for p in parts if p.startswith("sume-v1=")
)
body = b'{"event":"job.completed","job_id":"job_1"}'
ts = str(int(time.time()))
sig = hmac.new(b"s3cret", ts.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, ts, "sume-v1=" + sig, "s3cret"))
print(verify(body, ts, "sume-v1=" + sig, "wrong"))
try:
verify(body, ts, "sume-v1=" + sig, "")
except ValueError as e:
print("refused:", e)Reading the image after it passes
A job.completed delivery for an image job carries payload.artifacts[]. Each artifact has an id, a type (image), a content_type such as image/png and a url on media.sume.com. Failed and canceled deliveries use status ERROR and include an error object, so check the event name before you read artifacts.
Webhooks are terminal-only. There are no progress events, so keep a poll fallback with the job id if a delivery can be lost. The jobs docs describe the poll path, and the delivery receipt shows a signing_secret_fingerprint that you can compare with the dashboard when a signature does not verify.
Submitting the job
Send mode webhook with a public HTTPS webhook_url on POST /v1/images. Localhost, private-network and non-HTTPS URLs are rejected. A webhook-mode call returns the job envelope with 202, so the image arrives only through the delivery or the result endpoint.
{
"model": "bytedance-seed/seedream-5-lite",
"prompt": "A tidy desk seen from above, soft window light",
"mode": "webhook",
"webhook_url": "https://example.com/hooks/sume"
}Sources
Related posts
More in Developers
- Verify the Sume signature in the HTTP handler, not in the queue worker
A queue delay over 300 seconds makes a valid Sume signature look stale. Verify at receipt, enqueue the verified event, and use redeliver if a late check failed.
- Verify x-sume-webhook-signature in Node: sume-v1 HMAC, raw body
A node:crypto verifier for Sume's sume-v1 signature that refuses an empty secret, checks the 5-minute window, and accepts either entry during a rotation.
- verifyWebhook toleranceSeconds: 300 by default, and 0 turns replay off
In the Sume SDK, verifyWebhook rejects deliveries older than 300 seconds by default. toleranceSeconds 0 skips that check. When each setting is right.
- verifyWebhook toleranceSeconds 0 turns off the Sume replay check
In @sume-com/sdk, toleranceSeconds defaults to 300 and 0 skips the timestamp check. A runnable test shows an hour-old signed delivery passing only at 0.
Written by Sume