Python Sume webhook handler: stdlib verify and SQLite dedupe

A stdlib Python handler for Sume webhooks: verify that accepts rotation, then INSERT OR IGNORE on job_id so a retry runs once. Tested on 3.14 and 3.15.

5 min readSume
All posts

A Python handler for Sume webhooks needs two things from the standard library: hmac to check the sume-v1 signature over timestamp.rawbody, and sqlite3 to make the delivery idempotent. Insert the job_id with INSERT OR IGNORE into a table where it is the primary key, and read cursor.rowcount: 1 means this is the first copy, 0 means a retry you can acknowledge and skip. The 29-line module below returns an HTTP status you can plug into any framework.

Dedupe is not optional. Sume retries a failed delivery up to 10 times, and the redeliver endpoint sends the real terminal event again with a fresh timestamp and signature, so the same job can reach you more than once. The docs tell receivers to treat job_id as the idempotency key.

The handler

Pass the raw request bytes, never a parsed dict, plus the headers and your signing secret. It accepts a comma-separated header, which is how Sume signs during a secret rotation (one entry per live secret, newest first), and returns 401 for an empty secret rather than comparing against nothing.

import hashlib, hmac, json, sqlite3, time
db = sqlite3.connect("webhooks.db")
db.execute("CREATE TABLE IF NOT EXISTS seen (id TEXT PRIMARY KEY, event TEXT, body TEXT)")

def verify(raw: bytes, headers: dict, secret: str, tolerance: int = 300) -> bool:
    if not secret:
        return False  # refuse an empty secret
    h = {k.lower(): v for k, v in headers.items()}
    try:
        ts = int(h["x-sume-webhook-timestamp"])
        sigs = h["x-sume-webhook-signature"].split(",")
    except (KeyError, ValueError):
        return False
    if abs(time.time() - ts) > tolerance:
        return False
    mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s.strip(), f"sume-v1={mac}") for s in sigs)
def handle(raw: bytes, headers: dict, secret: str) -> int:
    if not verify(raw, headers, secret):
        return 401
    event = json.loads(raw)
    key = event.get("job_id") or event.get("run_id")
    if key:
        with db:  # one transaction: the insert is the dedupe check
            cur = db.execute("INSERT OR IGNORE INTO seen VALUES (?, ?, ?)",
                             (key, event.get("event"), raw.decode()))
        if cur.rowcount == 1:
            print("new event", event.get("event"), key)
    return 204

What I tested

The inputs were signed locally with the documented scheme, so this verifies my reading of the docs rather than a live Sume delivery.

handle() results, run 2026-10-02 on Python 3.14.7 and 3.15.0rc2
InputStatusHandled
Valid signature, first delivery204Yes
Same delivery again204No, ignored
Two signatures, valid one second204No, same job
Wrong secret401No
Empty secret401No
Timestamp outside 300 seconds401No

Production notes

Failure modes to plan for:

  • The insert commits before your real work runs. If the work then fails, the event is marked seen and will not run again, so add a status column and set it when the work finishes.
  • Move the work to a queue and return 204 in under 10 seconds, the per-attempt timeout.
  • SQLite is fine for one process. With several workers, use a Postgres unique index with ON CONFLICT DO NOTHING.
  • Route webhook.test events away: they have no job_id and never replay a real job.

Limits

This stores the whole body next to the id; if bodies contain result URLs you do not want in a table, store only the id and event. It does not read your signing secret for you: get it from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret with an account:read key, and keep status polling as the backup for events that never arrive.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume