Verify a Sume job webhook in Python for finished Omni clips
Check x-sume-webhook-signature on job.completed callbacks: HMAC SHA-256 over timestamp.raw_body, 5-minute tolerance, rotation-safe, empty secret rejected.

Sume signs each job webhook with HMAC SHA-256 over <timestamp>.<raw_body> and sends it as x-sume-webhook-signature: sume-v1=<hex>, with the timestamp in x-sume-webhook-timestamp. Verify the raw bytes, reject timestamps more than five minutes old, accept any matching entry during a secret rotation, and reject an empty secret. Use job_id as your idempotency key, because retries and redelivery can repeat an event.
What the docs specify
The Webhooks page lists three events for generation jobs (job.completed, job.failed, job.canceled), up to 10 delivery attempts 30 seconds apart with a 10 second timeout, and a signing secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret. Store it as SUME_COM_WEBHOOK_SIGNING_SECRET.
Submit an Omni clip with mode: "webhook" and a public HTTPS webhook_url, per the Video Router request shape.
| Item | Value |
|---|---|
| Signed string | timestamp, a dot, then the raw body |
| Header format | sume-v1=hex, comma-separated during rotation |
| Replay tolerance | 5 minutes suggested |
| Attempts | 10, 30 seconds apart, 10 s timeout |
| Idempotency key | job_id |
The verifier
This function returns false for an empty secret, a bad timestamp, a stale timestamp or no matching entry. It compares every entry in constant time. The last lines sign a sample body so you can run the file as is.
import hashlib, hmac, time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret:
return False
try:
t = int(ts)
except ValueError:
return False
if abs(int(time.time()) - t) > tol:
return False
mac = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256)
expected = "sume-v1=" + mac.hexdigest()
ok = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), expected):
ok = True
return ok
secret, body, now = "demo-secret", b'{"event":"job.completed"}', str(int(time.time()))
sig = "sume-v1=" + hmac.new(secret.encode(), now.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, now, sig, secret), verify(body, now, sig, ""))Operational notes
Read the body as bytes before any JSON parsing; re-serialized JSON will not match. Store the event durably, return 2xx, then process. Keep polling status_url as a fallback, since ten refused attempts leave the job complete but the delivery failed; POST /v1/jobs/{job_id}/webhook/redeliver re-sends the real terminal event with a fresh signature.
In your handler, read the artifact URL from the payload.artifacts array, and de-duplicate on job_id.
Sources
Related posts
More in Developers
- Verify a Sume video download against checksum_sha256 in Python
Stream a finished Sume artifact to disk, hash it with hashlib, and compare to checksum_sha256 from the job result. Skips cleanly when the field is null.
- Sume webhook signature header: why the sume-v1= prefix is checked
verifyWebhook only compares entries that start with sume-v1= and drops others, so a future scheme in the same header cannot break a receiver. A test proves it.
- v1/videos/models `created` is a catalog date, not a release date
Every model on Sume's /v1/videos/models shows created 1767225600, which is 2026-01-01. It is not when Gemini Omni 1.1 Flash or MiniMax H3 launched.
- Video filter stops at 300 seconds: which short-video lengths pass?
Sume's video filter refuses sources over 300 seconds. A 3-minute Short passes; a 10-minute TikTok ad source does not and needs a trim first.
Written by Sume