Verify a Sume job.completed webhook in Python (stdlib)

A stdlib Python verifier for Sume job webhooks: HMAC SHA-256 over timestamp.raw_body, five-minute tolerance, rotation-safe, and it refuses an empty secret.

4 min readSume
All posts

To verify a Sume job webhook in Python, compute HMAC SHA-256 of <timestamp>.<raw_body> with your signing secret, prefix it with sume-v1=, and compare it to every entry in the x-sume-webhook-signature header. Use the raw bytes of the body, not re-serialized JSON, and reject an empty secret or a timestamp outside your tolerance window.

What the docs specify

From the Webhooks docs.

Sume webhook signature facts, read 2026-10-02
ItemValue
Signed text<timestamp>.<raw_body>, HMAC SHA 256
Headersx-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>
RotationOne sume-v1= entry per live secret, comma separated; accept if any matches
Replay windowReject outside tolerance; five minutes is a reasonable default
Job eventsjob.completed, job.failed, job.canceled

The verifier

Stdlib only. hmac.compare_digest avoids timing leaks, and every entry is compared so a rotation does not reveal which secret matched.

import hashlib, hmac, time

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

Where the secret comes from

Read it from the Webhooks tab of the dashboard, or from GET /v1/webhooks/signing-secret with a key that has account:read. Store it in an environment variable such as SUME_COM_WEBHOOK_SIGNING_SECRET, never in source.

After it verifies

Fetch the image through the job result endpoint, since the webhook is a signal. Keep polling as a backup, as the docs advise, and dedupe on the job id because a delivery can repeat.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume