Verify a Sume webhook in Python with the standard library only
Check the sume-v1 HMAC in Python without a package: raw body, timestamp window, two signatures during rotation, and a refusal of an empty secret. 20 lines.

To verify a Sume job webhook in Python, compute HMAC-SHA256 over <timestamp>.<raw_body> with your signing secret, prefix it with sume-v1=, and compare it in constant time with every sume-v1= entry in the x-sume-webhook-signature header. Reject the call if the timestamp is more than 300 seconds from your clock, and refuse to run at all if the secret is empty.
The TypeScript SDK ships verifyWebhook; Python has no Sume package, so the Webhooks page gives the scheme and a TypeScript verifier, and this page ports it. The scheme is identical for job webhooks (job.*) and run webhooks (*.run.terminal), so one function covers both.
What the scheme requires
The values come from the Webhooks page and the Verifying webhooks page, read 2026-10-09.
| Item | Value |
|---|---|
| Signed data | <timestamp>.<raw_body>, the raw bytes before any JSON parse |
| Algorithm | HMAC SHA-256, hex digest |
| Timestamp header | x-sume-webhook-timestamp (seconds) |
| Signature header | x-sume-webhook-signature: sume-v1=<hex> |
| During a rotation | sume-v1=<new>,sume-v1=<previous>, newest first, for 24 hours |
| Replay window | Reject outside 300 s (five minutes is the suggested default) |
| Secret | Dashboard Webhooks tab or GET /v1/webhooks/signing-secret; env name SUME_COM_WEBHOOK_SIGNING_SECRET |
The verifier
The function takes bytes, not a parsed object. In Flask use request.get_data(); in FastAPI use await request.body(). Do not re-serialize parsed JSON, because key order and whitespace are part of the signed data.
import hashlib
import hmac
import time
def verify(raw: bytes, timestamp: str, header: str, secret: str, tolerance: int = 300) -> bool:
if not secret:
return False # never verify against an empty secret
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
if abs(int(time.time()) - ts) > tolerance:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256)
expected = f"sume-v1={mac.hexdigest()}".encode()
ok = False
for entry in (header or "").split(","):
if hmac.compare_digest(entry.strip().encode(), expected):
ok = True # keep looping so timing does not show which entry matched
return ok
if __name__ == "__main__":
body, secret, ts = b'{"event":"job.completed"}', "whsec_test", str(int(time.time()))
sig = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
print(verify(body, ts, f"sume-v1=deadbeef,sume-v1={sig}", secret)) # True
print(verify(body, ts, f"sume-v1={sig}", "")) # FalseWhy each guard is there
The empty-secret guard matters because an unset environment variable becomes an empty string in many setups. An HMAC with an empty key is still a valid HMAC, so without the guard a misconfigured service would accept signatures that anyone can compute.
The loop checks every entry because of the rotation window. For 24 hours after a rotation Sume signs with both secrets, newest first, so a receiver that still holds the old secret passes on the second entry, and a receiver that already holds the new secret passes on the first. A verifier that compares the whole header for equality fails during that window.
Return a fast 2xx after you have stored the event, and use job_id as the dedupe key. Return 401 on a failed check. Unknown event names should get a 204 and not a 500, so new event types do not start a retry storm.
Sources
Related posts
More in Developers
- The resolution enum has 8 values, but each Sume model takes 3 or 4
The /v1/videos resolution enum lists 360p to 4K. Wan, Seedance, H3, H3 Max and Omni each take a subset. Check supported_resolutions and what each extreme costs.
- Sume /v1/videos says cancelled, /v1/jobs says canceled: a status map
Two spellings and two vocabularies for the same Sume job: pending to cancelled on /v1/videos, queued to canceled on /v1/jobs. A table and a TypeScript mapping.
- Vidu 24-hour and FLUX 3 signed result links: copy the file first
Vidu result URLs last 24 hours and FLUX 3 signed URLs about 2 hours, or about 10 minutes by another line. Stream the Sume clip to disk and keep the job id.
- Vidu 540p has no Sume value: map Vidu resolution names
Vidu Q4 Preview offers 540p, 720p, 1080p, 2K and 4K. Sume's documented values are 480p, 720p, 768p, 1080p, 1K, 2K and 4K, per model. A lookup that fails loudly.
Written by Sume