Flask webhook receiver for Sume: verify sume-v1, refuse empty secret
A Flask route that verifies the Sume signature on the raw body, takes either rotation entry, checks the replay window, and will not boot without a secret.

To receive Sume job webhooks in Flask, read the raw request body, recompute HMAC-SHA256 over <timestamp>.<raw_body> with your workspace signing secret, and compare it to the sume-v1= entries in x-sume-webhook-signature. Refuse to start when the secret is empty; an empty secret makes every signature forgeable.
The route
Use request.get_data() before anything parses JSON, because a re-serialized body does not verify. During a rotation the header carries one entry per live secret, comma-separated, so accept any match. Reject timestamps outside five minutes.
import hashlib, hmac, os, time
from flask import Flask, request
SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not SECRET:
raise SystemExit("SUME_COM_WEBHOOK_SIGNING_SECRET is empty")
app = Flask(__name__)
def verify(raw: bytes, ts: str, header: str, tol: int = 300) -> bool:
try:
t = int(ts)
except ValueError:
return False
if abs(time.time() - t) > tol:
return False
digest = hmac.new(SECRET.encode(), f"{t}.".encode() + raw, hashlib.sha256)
want = "sume-v1=" + digest.hexdigest()
return any(hmac.compare_digest(e.strip(), want) for e in header.split(","))
@app.post("/hooks/sume")
def hook():
raw = request.get_data()
h = request.headers
if not verify(raw, h.get("x-sume-webhook-timestamp", ""),
h.get("x-sume-webhook-signature", "")):
return "bad signature", 401
return "", 204 # store the event first in real codeAfter the check
Return any 2xx after durably storing the event. Failed attempts are retried, up to 10 in total at a fixed delay, with a 10 second timeout per attempt, so do the real work after you answer. Use job_id as your idempotency key, and keep status polling as a backup for deliveries that never arrive.
Where the secret comes from
Read it on the dashboard Webhooks tab or from GET /v1/webhooks/signing-secret with a key carrying account:read. If a signature will not verify, compare the fingerprint in x-sume-webhook-secret-fingerprint with the one in the dashboard.
| Item | Value |
|---|---|
| Signed string | timestamp, a dot, then the raw body |
| Signature header | x-sume-webhook-signature: sume-v1=hex |
| Replay window | Five minutes suggested |
| Attempts | Up to 10, 10 second timeout each |
Sources
Related posts
More in Developers
- FLUX 3 bounding box to a mask_url: Python region edit on Sume
FLUX 3 Image boxes use [top, left, bottom, right] on a 0-1000 grid. Convert one to an RGBA mask with Pillow and run the region edit on Sume's GPT Image 2.5.
- FLUX 3 Image on OpenRouter: n=1, seed, base64 vs Sume
OpenRouter lists FLUX.3 Image with one image per call, a seed and base64 PNG output. How each differs from Sume's POST /v1/images, where FLUX 3 is not listed.
- FLUX 3 Image on Replicate: safety_tolerance 0-4 vs Sume
Replicate's FLUX 3 Image form has safety_tolerance 0-4, grounding, output_quality and 768sq-4k. Which of those inputs Sume's image API has, and what it returns.
- Gemini Omni edit 400: aspect_ratio is not supported, framing is kept
Sending aspect_ratio with a video_url edit on gemini-omni-flash-1.1 returns a 400 on Sume. Why the output keeps the source framing and what to send.
Written by Sume