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.

5 min readSume
All posts

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.

Webhook delivery facts for image jobs, as of 2026-10-08
ItemValue
Eventsjob.completed, job.failed, job.canceled (terminal only)
Timestamp headerx-sume-webhook-timestamp
Signature headerx-sume-webhook-signature: sume-v1=<hex>[,sume-v1=<hex>]
Signed string<timestamp>.<raw_body>
Replay windowFive minutes is a reasonable default
SecretSUME_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

All Developers posts

Written by Sume