Verify a Sume job webhook in Python: HMAC, timestamp, rotation

A Python verifier for Sume job webhooks: HMAC SHA 256 over timestamp.body, a 300-second window, key rotation and a refusal of an empty secret.

6 min readSume
All posts

How do you verify a Sume webhook from Python? Compute HMAC SHA 256 of <timestamp>.<raw_body> with your signing secret, prefix the hex with sume-v1=, and compare it with each entry in the x-sume-webhook-signature header. The Sume webhooks docs give the scheme and a TypeScript version; this is the same logic in Python.

It applies to TTS, music, audio detach and every other generation job, since all of them send the same terminal events.

What a delivery contains

Sume sends terminal events only: job.completed, job.failed and job.canceled. There are no progress or partial events. Two headers matter: x-sume-webhook-timestamp and x-sume-webhook-signature, which looks like sume-v1=<hex>. During a secret rotation the signature header carries one entry per live secret, comma separated, newest first, so accept the delivery if any entry matches.

Sume webhook delivery facts from the docs (read 2026-10-03)
ItemValue
Eventsjob.completed, job.failed, job.canceled
Signed string<timestamp>.<raw_body>, HMAC SHA 256, hex
Header formatsume-v1=<hex>, comma separated during rotation
Replay windowReject outside your tolerance; five minutes is the docs default
AttemptsUp to 10 total, fixed spacing of 30 s by default
Per-attempt timeout10 s
Idempotency key on your sidejob_id

The verifier

Run it against the raw bytes of the body, before any JSON parsing, because a re-serialised body will not match. It raises on an empty secret instead of silently accepting everything, and uses a constant-time comparison for every entry. Set SUME_COM_WEBHOOK_SIGNING_SECRET to run the demo at the bottom.

import hashlib
import hmac
import os
import time


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


if __name__ == "__main__":
    secret = os.environ["SUME_COM_WEBHOOK_SIGNING_SECRET"]
    body = b'{"event":"job.completed"}'
    now = str(int(time.time()))
    mac = hmac.new(secret.encode(), f"{now}.".encode() + body, hashlib.sha256)
    print(verify(body, now, "sume-v1=" + mac.hexdigest(), secret))

Operational details

Return a 2xx after you have durably stored the event; any other response or a network error is retried. Because a delivery can repeat, treat job_id as the idempotency key in your own store.

Ten refused attempts leave a failed delivery but a job that still reached its terminal state. Keep status polling available for the events that never arrive, as the jobs and results page recommends. A redeliver call re-sends the real terminal event with a fresh timestamp and signature, so your tolerance check must use the new timestamp rather than the original one.

Common mistakes

Four mistakes cause most failed checks. First, parsing the JSON and signing the re-serialised text, which changes whitespace and key order. Second, comparing the hex with an ordinary equality test instead of a constant-time one. Third, checking only the first entry of the signature header, which breaks during a secret rotation. Fourth, doing slow work before replying, since each attempt times out after 10 seconds and a slow endpoint burns the retry budget. Store the event, answer 2xx, and process it afterwards from your own queue.

Testing it

Use the dashboard's Send test, or POST /v1/webhooks/test-deliveries, to post a dummy signed webhook.test payload to a URL you type. It never replays a real job and its body has no job_id, so do not use it to test idempotency. For that, redeliver a real job. Read the signing secret from the dashboard's Webhooks tab, or from GET /v1/webhooks/signing-secret with an API key that has account:read; see the API reference for authentication.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume