Verify a Sume webhook signature in Fastify, Django and Flask
Each framework hands you the request body differently. Verify the Sume signature over the exact raw body, then parse it, in Fastify, Django and Flask.

A Sume webhook signature is computed over the exact body text, so verify it before anything parses the body. In Fastify that means a JSON content-type parser with parseAs: "string"; in Django and Flask, read the raw request body, verify it, then parse it.
Worked example: Django view for Sume webhooks: verify request.body, then parse
In Django, verify a Sume webhook against request.body before you parse it: build the HMAC over the timestamp, a dot and the body bytes, accept any matching sume-v1 entry, and return 204. The view needs csrf_exempt because Sume posts without a Django CSRF token.
Sume signs each delivery, so the receiver has one job: hand the untouched bytes to the HMAC check, reject anything that fails, store the event, and answer with a 2xx quickly. The rest of this page shows that for Django.
Why does a Django webhook fail verification?
Django's docs describe HttpRequest.body as the raw HTTP request body as a bytestring, and warn that accessing body after reading the request with read() or readline() raises RawPostDataException. So do not touch request.read() or a stream-parsing middleware before this view runs; read request.body once and reuse it.
The signature covers the timestamp, a dot, and the raw body. Sume's docs say a parsed-and-reserialized object does not verify, because key order and whitespace are part of the signed data. So the check must run before any JSON parsing.
What does a Django receiver look like?
The sample below reads the secret from SUME_COM_WEBHOOK_SIGNING_SECRET, the name the Sume docs use. It refuses to run the check when the secret is empty, enforces the 300-second replay window, and accepts the delivery when any sume-v1 entry matches, which keeps it working during a secret rotation.
import hashlib, hmac, os, time
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
def verify(body: bytes, ts: str, header: str) -> bool:
if not SECRET or not ts.isdigit():
return False
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(SECRET.encode(), ts.encode() + b"." + body, hashlib.sha256)
want = "sume-v1=" + mac.hexdigest()
return any(hmac.compare_digest(p.strip(), want) for p in header.split(","))
@csrf_exempt
@require_POST
def hook(request):
body = request.body # raw bytes, before any JSON parsing
ok = verify(body, request.headers.get("x-sume-webhook-timestamp", ""),
request.headers.get("x-sume-webhook-signature", ""))
if not ok:
return HttpResponse(status=401)
event = json.loads(body)
if event.get("event") in ("job.completed", "job.failed", "job.canceled"):
print("store", event.get("job_id")) # replace with a durable write
return HttpResponse(status=204)
What does Sume send and expect back?
Job webhooks carry job.completed, job.failed or job.canceled, and terminal events only: there are no progress deliveries. Run webhooks (Formats, Actions, Agent Completions) use the same signature scheme, so one verifier covers both. Route on the event field and answer unknown events with 204 so a new event type never becomes a retry storm.
| Step | Value from the Sume docs |
|---|---|
| Signed data | <timestamp>.<raw_body>, HMAC-SHA256 with your workspace secret |
| Signature header | x-sume-webhook-signature: sume-v1=<hex>; during a rotation, two comma-separated entries, newest first |
| Timestamp header | x-sume-webhook-timestamp; default replay window 300 seconds |
| Delivery limits | Up to 10 attempts, 30 s apart by default, 10 s timeout per attempt |
| Dedupe key | job_id (job events) or request_id |
How do I handle retries and lost deliveries?
Return a 2xx only after the event is stored durably. After ten refused attempts the job still reached its terminal state; only the notification failed. Keep a poll on the job's status_url as a fallback, or ask Sume to re-send the real terminal event with POST /v1/jobs/{job_id}/webhook/redeliver (needs the jobs:write scope). Redelivery carries a fresh timestamp and signature, so your replay window will accept it.
Use the dashboard's Send test (or POST /v1/webhooks/test-deliveries) to check the route first. It posts a dummy signed webhook.test payload that has no job_id, so your handler should treat it as a no-op rather than an error.
- Verify on raw bytes before parsing.
- Never run the check with an empty secret.
- Dedupe on job_id; redelivery is a normal event.
- If signatures fail, compare x-sume-webhook-secret-fingerprint with the fingerprint shown in the dashboard before you paste anything into a ticket.
Every framework in one table
This page replaces 3 posts that asked the same question with different numbers. Each row is one of them, with the answer it gave when it was published.
| Question | Answer |
|---|---|
| Django view for Sume webhooks: verify request.body, then parse | A Django view that verifies a Sume webhook against request.body: csrf_exempt, 300 s replay window, empty-secret refusal, multi-signature header, 204. |
| Fastify raw body for a Sume webhook: parseAs string and verifyWebhook | Fastify parses JSON before your handler, which breaks signatures. Use addContentTypeParser with parseAs string and verifyWebhook from @sume-com/sdk. |
| Flask webhook receiver for Sume job events: verify the signature | Verify a Sume job webhook in Flask: read request.get_data() raw, check the sume-v1 HMAC and 300 s window, refuse an empty secret, answer 204. |
Sources
Related posts
More in Developers
- Which MCP server lets Claude Code or Cursor generate video and images?
MCP servers that let Claude Code and Cursor make video and images: Sume, fal, Replicate, Runway, Higgsfield. Endpoints, sign-in, billing, setup.
- Idempotency keys for AI video APIs: retry without paying twice
An idempotency key makes a retried create return the original run or job instead of a second paid one. How Sume's Idempotency-Key works on each API.
- Signed webhooks for Sume video runs: events, retries, verification
Sume sends one HMAC-SHA256 signed POST when a Format, Action, or Agent Completion run completes or fails. Verify the raw body and dedupe on request_id.
- Spend caps for unattended AI agents: how Sume bounds each run
An unattended agent has no one to approve spend, so Sume caps generation per run: required on Agent Completions, and up to $500 on Format runs.
Written by Sume