Django webhook view for an AI video job: csrf_exempt and HMAC check
A Django view that verifies Sume's x-sume-webhook-signature over timestamp.raw_body, rejects an empty secret, and handles job.completed for a /v1/videos clip.

A Django view that receives an AI video webhook needs three things: @csrf_exempt (the caller is a server, not a browser form), the raw bytes in request.body for the signature check, and a fast 2xx so the sender stops retrying. Sume signs the raw JSON body with HMAC SHA-256 over <timestamp>.<raw_body> and sends x-sume-webhook-timestamp plus x-sume-webhook-signature: sume-v1=<hex> (Sume webhooks guide, read 2026-10-06).
To get webhooks for a video, send callback_url (an HTTPS URL) on POST /v1/videos. Sume posts its standard job envelope when the job reaches job.completed, job.failed or job.canceled; there are no progress events.
What is the full view?
Django documents HttpRequest.body as the raw request body bytes and HttpRequest.headers as a case-insensitive mapping, so the view reads both directly (Django request reference, read 2026-10-06). Never parse the JSON first and re-serialize it; the HMAC is over the exact bytes that were sent.
import hashlib, hmac, json, os, time
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 verified(raw: 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"." + raw,
hashlib.sha256).hexdigest()
want = f"sume-v1={mac}"
return any(hmac.compare_digest(want, p.strip())
for p in header.split(","))
@csrf_exempt
@require_POST
def sume_webhook(request):
h = request.headers
if not verified(request.body, h.get("x-sume-webhook-timestamp", ""),
h.get("x-sume-webhook-signature", "")):
return HttpResponse(status=401)
event = json.loads(request.body)
if event["event"] == "job.completed":
pass # enqueue a download task keyed by event["job_id"]
return HttpResponse(status=200)Which details are easy to get wrong?
- Empty secret: if the environment variable is unset, the code above returns 401 for everything instead of comparing against an empty key.
- Rotation: during a signing-secret rotation the signature header carries one
sume-v1=entry per live secret, comma separated, newest first. Theany(...)over the split header accepts either. - Replay window: Sume's guide suggests five minutes, which is the 300 in the code.
- Order of decorators:
csrf_exemptoutermost, so Django's CSRF middleware sees the exemption flag on the view it resolves (Django CSRF how-to, read 2026-10-06). - Do the work later: enqueue a task and return. The webhook is a delivery optimization; Sume's guide says to keep polling
status_urlas a backup for missed deliveries.
Where does the signing secret come from?
Reveal it on the Webhooks tab of the Sume dashboard, or read GET /v1/webhooks/signing-secret with an API key that has account:read. Store it as SUME_COM_WEBHOOK_SIGNING_SECRET; job webhooks and run webhooks share that one secret. Each delivery also carries x-sume-webhook-secret-fingerprint, so when a signature fails you can compare fingerprints with the dashboard instead of copying the secret around.
Duplicates are possible with any webhook sender, so key your handler on job_id. A SQLite insert-or-ignore on the job id is enough; in Django, a unique constraint on the job id column and get_or_create does the same.
Sources
Related posts
More in Developers
- Does a crossfade shorten a Short? Timeline start times explained
In Sume Timeline the declared start of each slot is authoritative, so a crossfade does not eat into the total length. How to keep a Short under its limit.
- Does my Short have audio? Check probe.has_audio before captions
Transcribing a silent clip returns inspect_source_has_no_audio. Probe has_audio first with a frames:false video inspect, then caption only clips with a voice.
- Envoy's 15 s route timeout vs a 30 s Sume sync call
Envoy's RouteAction timeout defaults to 15s. Sume sync mode can wait 30 s. Behind Envoy or Istio, use async, or raise the route timeout above the sync wait.
- Excel VBA: submit an AI video job and poll it with Application.OnTime
An Excel macro posts a Wan 3.0 job to Sume with ServerXMLHTTP, saves the job id in a cell and re-checks status with Application.OnTime without blocking Excel.
Written by Sume