Verify a Seedance 2.5 job webhook signature in Python
Check the x-sume-webhook-signature on a seedance-2.5 job.completed callback with HMAC SHA-256 over timestamp.body, in 20 lines of standard-library Python.

To verify a Sume job webhook for a seedance-2.5 clip, compute HMAC SHA-256 over <timestamp>.<raw_body> with your signing secret, prefix it with sume-v1=, and compare it to each comma-separated entry in x-sume-webhook-signature. Reject the call if the timestamp is more than five minutes old. The script below does this with the Python standard library.
The scheme comes from Sume's Webhooks docs, read 2026-10-02, and the submit options from the Video Router docs. The docs ship a TypeScript verifier; this is the same logic in Python.
How do I submit a Seedance 2.5 job with a callback?
Send mode: "webhook" with a public HTTPS webhook_url on POST /v1/video-router/generate. Sending webhook_url without a mode also selects webhook mode. Localhost, private-network and non-HTTPS URLs are rejected. Only three events exist: job.completed, job.failed and job.canceled, with no progress events.
What does the verifier look like?
Get the secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret. It refuses an empty secret on purpose: an empty key would make every forged body verify. During a secret rotation the header carries one entry per live secret, so the loop accepts any match.
The last three lines are a self-test with a made-up secret; remove them in production.
import hashlib, hmac, time
def verify(raw_body: bytes, timestamp: str, header: str, secret: str,
tolerance: int = 300) -> bool:
if not secret:
raise ValueError("empty signing secret")
try:
ts = int(timestamp)
except ValueError:
return False
if abs(time.time() - ts) > tolerance:
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 ok
body = b'{"event":"job.completed"}'
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(b"demo-secret", f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
print(verify(body, ts, sig, "demo-secret"))What else should the receiver do?
Verify against the raw request body, not re-serialized JSON, since the signature covers the exact bytes. Return any 2xx only after storing the event, and key your own records on job_id, because retries and redelivery repeat the same job.
Keep polling status_url as a backup; the docs call delivery an optimization, never the only recovery path. If a callback never shows up, the missed-webhook checklist lists what to check.
Store the signing secret in an environment variable such as SUME_COM_WEBHOOK_SIGNING_SECRET, the name the docs use, never in source control. If verification fails after a rotation, compare the fingerprint header with the one in the dashboard before debugging the code.
| Header | Meaning |
|---|---|
x-sume-webhook-timestamp | Unix seconds used in the signed string |
x-sume-webhook-signature | sume-v1=<hex>, one entry per live secret, newest first |
x-sume-webhook-secret-fingerprint | Fingerprint to compare with the dashboard when a signature fails |
Sources
Related posts
More in Developers
- Seedance 2.5 reference audio alone returns 400: add an image
On Sume's Video Router, reference_audio_urls with no reference image or video returns 400. Pair the audio with an image or clip, within 3 audio and 12 total.
- Seedance 2.5 seed, size and provider options: why Sume returns 400
Sume rejects seed, size and non-empty provider.options on seedance-2.5 with 400 instead of dropping them. What to send to repeat a clip or fix the frame size.
- Seedance 2.5 sync mode: a 30-second wait, then poll
mode sync on the Video Router blocks at most 30 seconds. A Seedance 2.5 job can outlast it, so read sync.timed_out and poll status_url without resubmitting.
- Seedance aspect ratios on Sume: 3:2 and 9:21 are not on every model
Sume accepts nine aspect ratios in total, but each video model lists its own subset. Check supported_aspect_ratios before you request 3:2 or 9:21.
Written by Sume