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.

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-timestampis 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 asSUME_COM_WEBHOOK_SIGNING_SECRET.
| Header | Use |
|---|---|
x-sume-webhook-signature | sume-v1=<hex> entries, comma-separated during rotation |
x-sume-webhook-timestamp | Unix seconds; part of the signed string |
x-sume-webhook-secret-fingerprint | Compare 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
- Python urllib: honor retry-after on a Sume 429, back off without it
A stdlib retry for GET calls that sleeps for retry-after when the 429 carries it and for a capped exponential delay when it does not, with jitter.
- Python urllib: POST /v1/images, 200 or 202, after Imagen 4 Fast
imagen-4.0-fast-generate-001 ended Aug 17. A stdlib Python call to Sume's image route that reads the status code, then polls the job when the answer is 202.
- queue_full 429 on a Sume submit: the reservation is released
A 429 queue_full releases or refunds the failed admission's reservation. Check refunded_usd_micros in /v1/usage, then retry with the same Idempotency-Key.
- Can I submit 100 AI video jobs at once? Queue limits by plan
Accepted capacity is slots plus queue: 6 on Free, 24 on Pro, 48 on Startup, 120 on Scale. Submit 100 at once and 94, 76, 52 or 0 get 429 queue_full.
Written by Sume