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.

4 min readSume
All posts

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.

Headers on a delivery, from Sume's Webhooks docs, read 2026-10-02.
HeaderMeaning
x-sume-webhook-timestampUnix seconds used in the signed string
x-sume-webhook-signaturesume-v1=<hex>, one entry per live secret, newest first
x-sume-webhook-secret-fingerprintFingerprint to compare with the dashboard when a signature fails

Sources

Related posts

More in Developers

All Developers posts

Written by Sume