Verify a Sume webhook in Python when the secret is rotating
A Sume webhook can carry two sume-v1 signatures for 24 hours after rotation. A FastAPI verifier that accepts either, rejects an empty secret, checks the clock.

Split the signature header on commas, and accept the delivery if any sume-v1= entry matches the HMAC SHA-256 of <timestamp>.<raw_body>. For 24 hours after you rotate the signing secret, Sume signs each delivery with both secrets, newest first, so a verifier that compares the whole header for equality fails on every request.
The Python below uses only the standard library plus FastAPI, refuses an empty secret, and checks a 300-second replay window.
What the rotation window does
You rotate with the dashboard Webhooks tab or POST /v1/webhooks/signing-secret/rotate using a key with account:write. The header then looks like sume-v1=<new>,sume-v1=<old>. After the window the old secret stops working.
x-sume-webhook-secret-fingerprint names the new secret from the moment you rotate. It tells you which secret to move to, not which ones Sume still accepts.
| When | Signature header | Verifier needs |
|---|---|---|
| Before rotation | One sume-v1 entry | Current secret |
| First 24 hours | Two entries, newest first | Either secret |
| After the window | One entry | New secret only |
The verifier
Run it with uvicorn app:app after pip install fastapi uvicorn. It compares every entry so the timing does not reveal which one matched.
import hashlib, hmac, os, time
from fastapi import FastAPI, HTTPException, Request
SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not SECRET:
raise RuntimeError("SUME_COM_WEBHOOK_SIGNING_SECRET is required")
app = FastAPI()
def verify(raw: bytes, ts: str, header: str, tolerance=300) -> bool:
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
digest = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
want = f"sume-v1={digest}"
ok = False
for part in header.split(","):
ok |= hmac.compare_digest(part.strip(), want)
return ok
@app.post("/hooks/sume", status_code=204)
async def hook(request: Request):
raw = await request.body()
if not verify(raw, request.headers.get("x-sume-webhook-timestamp", ""),
request.headers.get("x-sume-webhook-signature", "")):
raise HTTPException(status_code=401)Upgrade before you rotate
Deploy the multi-entry verifier first, rotate second. If you rotate twice inside one window, Sume retires the older secret at once, which is how you make a leak really stop.
Sources
Related posts
More in Developers
- verifyWebhook in a fetch handler: four rules, 204 for unknown events
Use @sume-com/sdk verifyWebhook on the raw body, await it, treat false as 401 and answer unknown events with 204. A runnable handler for Workers, Deno and Node.
- Video API callback_url and Idempotency-Key: a sume/auto job in curl
Submit a video job on Sume with callback_url instead of polling, add an Idempotency-Key so retries are safe, and let sume/auto pick the model. Curl and errors.
- Will polling video jobs trigger a 429? Reads and writes differ
Each Sume API key has a write budget and a read budget 40 times larger, so a status-poll loop cannot 429 your submits. Poll math, headers and a 429 backoff.
- Voice agent narrates a long task: poll a Sume video job meanwhile
Decagon Voice 3 narrates progress during long tasks. The same pattern for a voice agent that starts a Sume job: submit, speak, poll jobs_wait.
Written by Sume