Live-commerce clip webhook: verify the Sume signature in Python
Verify a Sume webhook in Python: HMAC SHA256 over timestamp.raw_body, any sume-v1 entry, a 300-second window, and reject an empty secret.

To verify a Sume webhook that reports a finished live-commerce clip, compute HMAC SHA256 over <timestamp>.<raw_body> with your signing secret, compare it to each sume-v1= entry in x-sume-webhook-signature, reject a timestamp more than five minutes old, and refuse to run at all if the secret is empty. A trim, caption or render submitted with webhook_url is delivered as job.completed, job.failed or job.canceled.
Source: Webhooks: signed job callbacks, which gives the TypeScript version; this is the same logic in Python.
What Sume signs
Sume signs the raw JSON body, not the parsed object, so read the request bytes before any JSON middleware reformats them. The headers are x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. During a secret rotation the signature header has one entry per live secret, newest first, separated by commas; accept the delivery if any entry matches. Each delivery also carries x-sume-webhook-secret-fingerprint, which you can compare to the fingerprint next to the secret in the dashboard when verification fails.
Why reject an empty secret
An empty secret is a valid HMAC key, so a verifier with an unset environment variable computes a signature anyone can forge. Failing closed when the secret is blank turns a silent hole into a loud error at startup. The same function should also compare in constant time and ignore entries that do not start with sume-v1=.
The function below takes the raw bytes, the two header values and the secret, and returns a boolean. It includes a self-test with a signature it generates itself, so you can run the file as it is.
import hashlib, hmac, time
def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
if not secret:
raise ValueError("signing secret is empty")
if not ts.isdigit() or abs(int(time.time()) - int(ts)) > tol:
return False
msg = ts.encode() + b"." + raw
want = "sume-v1=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
ok = False
for entry in header.split(","):
entry = entry.strip()
if entry.startswith("sume-v1=") and hmac.compare_digest(entry, want):
ok = True
return ok
if __name__ == "__main__":
body = b'{"event":"job.completed","job_id":"job_demo"}'
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(b"s3cret", ts.encode() + b"." + body,
hashlib.sha256).hexdigest()
print(verify(body, ts, "sume-v1=old," + sig, "s3cret"))
try:
verify(body, ts, sig, "")
except ValueError as e:
print("rejected:", e)
After it verifies
Store the event durably, then return any 2xx. Sume retries network errors and non-2xx responses, up to 10 attempts in total, so use job_id as your idempotency key and handle a repeated delivery as a no-op. If you do heavy work, such as copying the finished clip, do it after you acknowledge, not before.
| Input | Where it comes from | Rule |
|---|---|---|
| Raw body | Request bytes | Do not re-serialize JSON |
| Timestamp | x-sume-webhook-timestamp | Reject past 300 seconds |
| Signature | x-sume-webhook-signature | Accept any sume-v1 entry |
| Secret | Dashboard or GET /v1/webhooks/signing-secret | Reject when empty |
| Idempotency | job_id | Treat repeats as no-ops |
Where this fits in a live-commerce pipeline
A common shape is: cut the product moment with trim, burn the price with caption cues, and have each job post to one endpoint. The verifier is the only code that touches the secret, so test it once and share it. The stored post on live-commerce host videos by API shows the Format version with a webhook.
Sources
Related posts
More in Use cases
- Only 12 left badge over a product clip: compose overlay, $0.02
A low-stock badge on a product clip is $0.02 per variant with Sume compose overlay. Set position, width_ratio and margin_ratio; re-render as stock drops.
- Meta One plan prices vs 20 ad clips on Sume: $6.40
Meta One's four plans run $14.99 to $499 a month. Twenty clips through Sume's trim, caption and timeline jobs cost $6.40. What the two numbers measure.
- AI video with native audio vs a scripted voiceover: exact words
Seedance 2.5, MiniMax H3 and Kling make sound with the picture. When a line must be word-exact, generate the voiceover with Sume TTS and join it on a timeline.
- One Sume TTS job: 20,000 characters is about 26 minutes of narration
At 750 characters per minute, a 20,000-character job covers about 26.7 minutes for 95 cents. Compare Shorts at 3 min, TikTok at 10 min and Reels at 20 min.
Written by Sume