Verify a Sume avatar video webhook in Python (HMAC SHA-256)

A Python verifier for avatar video webhooks: timestamp tolerance, rotation-safe comparison, and a hard refusal when the signing secret is empty.

5 min readSume
All posts

To verify a Sume webhook in Python, compute an HMAC SHA-256 over <timestamp>.<raw_body> with your workspace signing secret, prefix the hex digest with sume-v1=, and compare it in constant time with every sume-v1= entry in the x-sume-webhook-signature header. Reject the request if the timestamp is more than five minutes old, and reject it outright if your secret is empty. An empty secret signs nothing, but an attacker can sign with an empty key, so a verifier that does not check for it will accept forged bodies.

Avatar videos take longer than the 30-second wait that sync mode allows, so a webhook is the natural way to learn that a render finished. Keep the verifier in its own module with a unit test for the three cases that matter: a valid signature passes, a one-character change to the body fails, and an empty secret fails even when the signature was computed with an empty key.

The verifier

This follows the scheme documented on the Sume webhooks page. It accepts a rotating secret: during rotation the header carries one entry per live secret, comma-separated.

import hashlib
import hmac
import time


def verify(raw_body: bytes, timestamp: str, header: str,
           secret: str, tolerance: int = 300) -> bool:
    if not secret:
        return False  # never verify against an empty secret
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(int(time.time()) - ts) > tolerance:
        return False
    mac = hmac.new(secret.encode(), digestmod=hashlib.sha256)
    mac.update(f"{ts}.".encode() + raw_body)
    expected = "sume-v1=" + mac.hexdigest()
    ok = False
    for entry in header.split(","):
        if hmac.compare_digest(entry.strip(), expected):
            ok = True
    return ok

Wire it to the request

Sume sends only terminal events for generation jobs: job.completed, job.failed and job.canceled. Failed and canceled events carry status: "ERROR" and an error object.

  • Read the raw bytes of the body before any JSON parsing. Re-serialized JSON will not match the signature.
  • Pass the headers x-sume-webhook-timestamp and x-sume-webhook-signature.
  • Get the secret from the Webhooks tab of the dashboard or GET /v1/webhooks/signing-secret, and store it in SUME_COM_WEBHOOK_SIGNING_SECRET.
  • Persist the event, then return any 2xx. Use job_id as your idempotency key because the same event can arrive more than once.

Submit the job with a webhook

Send mode: "webhook" and a public HTTPS webhook_url when you create the avatar video. Localhost, private-network and non-HTTPS URLs are rejected.

Delivery behavior from Webhooks.
BehaviorValue
AttemptsUp to 10
SpacingFixed delay, 30 seconds by default
Per-attempt timeout10 seconds
Replay window to enforce5 minutes is a reasonable default
After attempts run outJob keeps its real state; use Redeliver or poll status_url

Limits

A webhook is an optimization, never your only recovery path. Keep polling status_url for events that never arrive, and treat a failed signature check as a reason to compare the x-sume-webhook-secret-fingerprint header with the fingerprint in the dashboard, not as a reason to disable verification. A job that reached completed while your endpoint was down can be re-sent with POST /v1/jobs/{job_id}/webhook/redeliver.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume