Verify a Sume avatar video webhook signature in Python

A Python verifier for Sume job webhooks: HMAC SHA-256 over timestamp.raw_body, sume-v1 entries, a 5-minute window, and a refusal when the secret is empty.

5 min readSume
All posts

Sume signs the raw JSON body of each job webhook with HMAC SHA-256 over the string timestamp, a dot, then the raw body. Read x-sume-webhook-timestamp and x-sume-webhook-signature, rebuild the digest with your signing secret, and compare it with each sume-v1 entry in the header. The Python function below also refuses to verify when the secret is empty.

What Sume sends

For an avatar video you submit with mode webhook and a public HTTPS webhook_url, Sume sends terminal events only: job.completed, job.failed or job.canceled. There are no progress deliveries. From the Webhooks page, read 2026-10-08:

Webhook headers, as of 2026-10-08
HeaderMeaning
x-sume-webhook-timestampUnix seconds used in the signed string
x-sume-webhook-signatureOne or more sume-v1=<hex> entries, comma separated
x-sume-webhook-secret-fingerprintFingerprint of the secret, for debugging a mismatch

Rules your verifier must follow

  • Sign the raw body bytes as received, not re-serialized JSON.
  • Reject timestamps outside a replay window; five minutes is a reasonable default.
  • During secret rotation the header carries one entry per live secret, newest first; accept the delivery if any entry matches.
  • Compare every entry with a constant-time function.
  • Return any 2xx only after storing the event durably; use job_id as your idempotency key.

Python verifier

Pass the raw request body as a string. In a web framework, read the body before parsing it as JSON.

import hashlib
import hmac
import time


def verify_sume_webhook(raw_body, timestamp, header, secret, tolerance=300):
    if not secret:
        return False
    try:
        ts = int(timestamp)
    except (TypeError, ValueError):
        return False
    if abs(int(time.time()) - ts) > tolerance:
        return False
    digest = hmac.new(
        secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256
    ).hexdigest()
    expected = f"sume-v1={digest}"
    matched = False
    for entry in header.split(","):
        if hmac.compare_digest(entry.strip(), expected):
            matched = True
    return matched

Operating notes

Sume retries up to 10 attempts in total, with a fixed delay (30 seconds by default) and a 10-second timeout for each attempt. After that you have a failed delivery for a job that still finished, so keep polling status_url as a fallback. Your signing secret is on the dashboard Webhooks tab, and the docs name the environment variable SUME_COM_WEBHOOK_SIGNING_SECRET. If verification fails, compare the fingerprint header with the fingerprint shown next to the secret.

Testing the verifier

Test with three cases. First, a valid delivery built with your secret and a current timestamp should return true. Second, an old timestamp, such as one ten minutes ago, should return false. Third, an empty secret should always return false, even if the other inputs look valid, because an empty key would let anyone forge a signature.

During rotation, build a header with two entries joined by a comma and confirm the function accepts it when either entry is right. Keep the check constant time: the loop above compares every entry instead of returning on the first match.

Finally, do the work after verification. Store the event by job_id, return a 2xx, and process asynchronously. Sume allows 10 seconds per attempt, so a slow handler wastes the retry budget.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume