Verify a Sume video callback_url webhook in Python, rotation-aware

A Python verifier for a Sume video job callback: HMAC over timestamp.body, five-minute window, rotation entries, and an empty secret refused.

4 min readSume
All posts

Short answer

Sume signs a video job callback with HMAC SHA-256 over <timestamp>.<raw_body> and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. Verify the raw bytes, reject timestamps more than five minutes away, and accept the delivery if any sume-v1= entry in the header matches, because the header carries one entry per live secret during rotation. Per the video docs, callback_url must be HTTPS and Sume posts when the job reaches a terminal state.

The verifier

It refuses an empty secret, parses the timestamp, checks the window, computes the expected value and compares it against every entry in constant time. The demo signs its own sample body to show the round trip; replace it with the raw request body and headers from your framework.

import asyncio, hashlib, hmac, os, time

def verify(raw, ts, header, secret, tol=300):
    if not secret:
        raise ValueError("empty signing secret")
    try:
        sent = int(ts)
    except ValueError:
        return False
    if abs(time.time() - sent) > tol:
        return False
    mac = hmac.new(secret.encode(), f"{sent}.".encode() + raw,
                   hashlib.sha256).hexdigest()
    ok = False
    for entry in header.split(","):
        ok = hmac.compare_digest(entry.strip(), "sume-v1=" + mac) or ok
    return ok

async def main():
    secret = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
    raw = b'{"job_id":"job_demo"}'
    ts = str(int(time.time()))
    mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw,
                   hashlib.sha256).hexdigest()
    print(verify(raw, ts, "sume-v1=" + mac, secret))

asyncio.run(main())

Facts to get right

Sume webhook signature facts (Sume docs, read 2026-10-04)
ItemValue
Signed stringtimestamp, a dot, then the raw JSON body
AlgorithmHMAC SHA-256, hex, prefixed sume-v1=
Replay windowReject outside about five minutes
RotationHeader holds one entry per live secret, comma-separated, newest first
Secret nameSUME_COM_WEBHOOK_SIGNING_SECRET, read from the dashboard Webhooks tab
ResponseReturn a 2xx after storing the event durably

Common mistakes

The first is verifying the parsed and re-serialized JSON instead of the raw bytes; key order or spacing changes the digest. The second is comparing with ==, which leaks timing. The third is checking only the first header entry, which fails during a rotation when the newest entry is the one you do not yet hold. The fourth is accepting an empty secret, which turns the check into a signature anyone can forge: the function above raises instead.

Idempotent handling

Use the job_id as your key and ignore a second delivery of the same terminal state. Store the event before returning 2xx; Sume retries non-2xx responses and network errors. After the event, fetch the video with the content endpoint or the unsigned_urls on the job, using your API key header.

Testing it

Test three cases before you go live: a correct signature passes, a body changed by one byte fails, and a timestamp ten minutes old fails. Add a fourth for rotation by building a header with two entries, a wrong one first and the right one second, and assert it passes. A fifth test should assert that an empty secret raises, since silently accepting everything is the worst failure this code can have.

Caveats

The envelope is Sume's standard job webhook, not OpenRouter's video.generation.* shape, so do not reuse an OpenRouter parser. Delivery is on terminal states only, so a webhook is not a progress feed.

Related posts

More in Developers

All Developers posts

Written by Sume