Live-commerce clip webhook: verify the Sume signature in Python

Verify a Sume webhook in Python: HMAC SHA256 over timestamp.raw_body, any sume-v1 entry, a 300-second window, and reject an empty secret.

5 min readSume
All posts

To verify a Sume webhook that reports a finished live-commerce clip, compute HMAC SHA256 over <timestamp>.<raw_body> with your signing secret, compare it to each sume-v1= entry in x-sume-webhook-signature, reject a timestamp more than five minutes old, and refuse to run at all if the secret is empty. A trim, caption or render submitted with webhook_url is delivered as job.completed, job.failed or job.canceled.

Source: Webhooks: signed job callbacks, which gives the TypeScript version; this is the same logic in Python.

What Sume signs

Sume signs the raw JSON body, not the parsed object, so read the request bytes before any JSON middleware reformats them. The headers are x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. During a secret rotation the signature header has one entry per live secret, newest first, separated by commas; accept the delivery if any entry matches. Each delivery also carries x-sume-webhook-secret-fingerprint, which you can compare to the fingerprint next to the secret in the dashboard when verification fails.

Why reject an empty secret

An empty secret is a valid HMAC key, so a verifier with an unset environment variable computes a signature anyone can forge. Failing closed when the secret is blank turns a silent hole into a loud error at startup. The same function should also compare in constant time and ignore entries that do not start with sume-v1=.

The function below takes the raw bytes, the two header values and the secret, and returns a boolean. It includes a self-test with a signature it generates itself, so you can run the file as it is.

import hashlib, hmac, time

def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
    if not secret:
        raise ValueError("signing secret is empty")
    if not ts.isdigit() or abs(int(time.time()) - int(ts)) > tol:
        return False
    msg = ts.encode() + b"." + raw
    want = "sume-v1=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    ok = False
    for entry in header.split(","):
        entry = entry.strip()
        if entry.startswith("sume-v1=") and hmac.compare_digest(entry, want):
            ok = True
    return ok

if __name__ == "__main__":
    body = b'{"event":"job.completed","job_id":"job_demo"}'
    ts = str(int(time.time()))
    sig = "sume-v1=" + hmac.new(b"s3cret", ts.encode() + b"." + body,
                                hashlib.sha256).hexdigest()
    print(verify(body, ts, "sume-v1=old," + sig, "s3cret"))
    try:
        verify(body, ts, sig, "")
    except ValueError as e:
        print("rejected:", e)

After it verifies

Store the event durably, then return any 2xx. Sume retries network errors and non-2xx responses, up to 10 attempts in total, so use job_id as your idempotency key and handle a repeated delivery as a no-op. If you do heavy work, such as copying the finished clip, do it after you acknowledge, not before.

Webhook verification inputs from Sume's Webhooks page, read 2026-10-06.
InputWhere it comes fromRule
Raw bodyRequest bytesDo not re-serialize JSON
Timestampx-sume-webhook-timestampReject past 300 seconds
Signaturex-sume-webhook-signatureAccept any sume-v1 entry
SecretDashboard or GET /v1/webhooks/signing-secretReject when empty
Idempotencyjob_idTreat repeats as no-ops

Where this fits in a live-commerce pipeline

A common shape is: cut the product moment with trim, burn the price with caption cues, and have each job post to one endpoint. The verifier is the only code that touches the secret, so test it once and share it. The stored post on live-commerce host videos by API shows the Format version with a webhook.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume