Sume job webhook receiver in plain Python, no framework

A 30-line stdlib http.server receiver for Sume job webhooks: raw-body HMAC, 300-second window, rotation-safe header, refuses an empty secret, answers 204.

4 min readSume
All posts

You can verify a Sume job webhook with the Python standard library alone: read the raw body, compute HMAC-SHA256 over <timestamp>.<raw_body> with your signing secret, compare to every sume-v1= entry in the signature header, and reject anything outside a five-minute window. Answer 204 once the event is stored.

This is useful for a small worker, a container health-checked sidecar, or a test double in CI. In production behind a framework, keep the same four rules: raw body, constant-time compare, replay window, and a refusal to start with an empty secret.

The four rules from the docs

Sume's webhook page and the SDK verifier page agree on the checks. Everything below is stated there; none of it is specific to Python.

  • Sign over the raw bytes. A parsed and re-serialized body will not verify, because key order and whitespace are part of the signed data.
  • The header is x-sume-webhook-signature: sume-v1=<hex>; during a secret rotation it carries one entry per live secret, newest first, comma-separated. Accept the delivery if any entry matches.
  • x-sume-webhook-timestamp is a Unix time in seconds. Reject it outside your tolerance; five minutes is the documented default.
  • Your secret comes from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret, stored as SUME_COM_WEBHOOK_SIGNING_SECRET.
Headers a Sume job webhook carries (read 2026-10-07)
HeaderUse
x-sume-webhook-signaturesume-v1=<hex> entries, comma-separated during rotation
x-sume-webhook-timestampUnix seconds; part of the signed string
x-sume-webhook-secret-fingerprintCompare with the dashboard when a signature fails

The receiver

The process exits at startup when the secret is empty, so a missing environment variable cannot turn into a server that accepts everything. Run it, point a public HTTPS tunnel at it, and use the dashboard's Send test to see a webhook.test event arrive.

import hashlib, hmac, os, time
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not SECRET:
    raise SystemExit("SUME_COM_WEBHOOK_SIGNING_SECRET is empty")

def verify(body, ts, header, tolerance=300):
    try:
        stamp = int(ts)
    except ValueError:
        return False
    if abs(time.time() - stamp) > tolerance:
        return False
    mac = hmac.new(SECRET.encode(), f"{stamp}.".encode() + body, hashlib.sha256)
    expected = "sume-v1=" + mac.hexdigest()
    return any(hmac.compare_digest(e.strip(), expected) for e in header.split(","))

class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("content-length", "0")))
        good = verify(body, self.headers.get("x-sume-webhook-timestamp", ""),
                      self.headers.get("x-sume-webhook-signature", ""))
        self.send_response(204 if good else 401)
        self.end_headers()

HTTPServer(("", 8080), Hook).serve_forever()

What to do after the 204

Return the 2xx only after you have stored the event durably; Sume retries network errors and non-2xx answers up to ten attempts with a 10-second timeout each. Then dedupe on job_id, which the docs name as the idempotency key, and route on the event field: job.completed, job.failed, job.canceled. Answer unknown events with 204, not 500, so a future event type does not start a retry storm.

A failed or canceled job webhook carries status: "ERROR" and an error object, so branch on event, not on status.

Limits of this sketch

http.server is single-threaded here and has no TLS, so put it behind a reverse proxy that terminates HTTPS; Sume only delivers to public HTTPS URLs. If a signature fails, compare x-sume-webhook-secret-fingerprint with the fingerprint next to the secret in the dashboard before you suspect your code. Delivery is an optimization: keep GET /v1/jobs/:id/status polling available for events that never arrive.

Testing it without Sume

You can test the verifier offline by signing a sample body yourself with the same secret and posting it with curl. If the 204 comes back for a fresh timestamp and 401 for a timestamp ten minutes old, the replay window works. Also post it with the secret changed by one character, and with an empty signature header; both must be 401.

Do this in CI so that a refactor of the verifier cannot quietly accept unsigned traffic, which is the failure that costs the most and shows nothing in logs.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume