Verify a Sume video callback signature in Python, empty secret refused

A short stdlib Python check for x-sume-webhook-signature on a /v1/videos callback: HMAC SHA-256 over timestamp.raw_body, rotated sume-v1 entries accepted.

5 min readSume
All posts

Sume signs a video callback with HMAC SHA-256 over <timestamp>.<raw_body> and sends the result in x-sume-webhook-signature as sume-v1=<hex>, with the timestamp in x-sume-webhook-timestamp. A verifier must read the raw bytes, recompute the HMAC with your signing secret, compare in constant time, and refuse an empty secret instead of silently accepting every request.

What arrives on the callback

For POST /v1/videos, you send callback_url (HTTPS only) and Sume POSTs to it when the job reaches a terminal state. The payload is the standard Sume job webhook envelope, not the OpenRouter video.generation.* envelope. Facts below are from the Sume webhooks and video pages, read 2026-10-09.

Video callback signature facts, as of 2026-10-09
ItemValue
Signed bytestimestamp, a dot, then the raw JSON body
AlgorithmHMAC SHA-256
Timestamp headerx-sume-webhook-timestamp
Signature headerx-sume-webhook-signature: sume-v1=<hex>
During secret rotationComma-separated sume-v1 entries, newest first; accept any match
Replay windowReject outside your tolerance; five minutes is the docs' default
Eventsjob.completed, job.failed, job.canceled (terminal only)

The verifier

This is plain Python with no awaits. It raises on an empty secret, rejects stale or malformed timestamps, and compares every sume-v1= entry so a rotation does not break delivery.

import hashlib
import hmac
import time


def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
    if not secret:
        raise ValueError("signing secret is empty; refuse to verify")
    try:
        stamp = int(ts)
    except (TypeError, ValueError):
        return False
    if abs(time.time() - stamp) > tol:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    want = mac.hexdigest()
    for part in header.split(","):
        part = part.strip()
        if part.startswith("sume-v1=") and hmac.compare_digest(part[8:], want):
            return True
    return False

Testing the verifier

Test three cases before you deploy. First, a good signature built with the same formula passes. Second, a body altered by one byte fails. Third, an empty secret raises, which proves the guard is in place. Add a fourth case with a timestamp 10 minutes old to see the replay window reject it.

Pass the raw request bytes to the function. Most web frameworks parse JSON before your handler sees it; if you re-serialise the parsed body, key order and spacing can change and the HMAC will not match. In a stdlib handler read Content-Length bytes from the socket; in a framework use the raw-body accessor.

Respond with a 2xx quickly and do the heavy work after. Return a non-2xx for a bad signature so the failure is visible in the delivery status. The webhook delivery states are pending, delivering, delivered, retrying, failed and exhausted, so a slow or failing endpoint is retried before it is marked exhausted.

Operational notes

Read the signing secret from the dashboard Webhooks tab or from GET /v1/webhooks/signing-secret with an API key that has account:read; store it in an environment variable and fail the process at start-up if it is unset. Each delivery also carries x-sume-webhook-secret-fingerprint, which you can compare with the fingerprint next to the secret in the dashboard when a signature does not verify.

Keep polling as a fallback. A webhook can be retried or exhausted, so a job that you expected a callback for can be read at GET /v1/videos/{jobId}.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume