Sume video callback not verifying? Compare the secret fingerprint
Every Sume job webhook carries x-sume-webhook-secret-fingerprint. Compare it with the dashboard, accept two signatures in rotation, refuse an empty secret.

If a callback from a Sume video job does not verify, check the secret fingerprint before you touch the secret. Every delivery carries an x-sume-webhook-secret-fingerprint header, and the delivery receipt repeats it as webhook_delivery.signing_secret_fingerprint. Compare it with the fingerprint shown beside the secret in the dashboard. If the two match, you are using the right secret and the bug is in how you build the signed string. If they differ, you have the wrong secret. Neither side ever has to send the secret itself.
Pass callback_url on POST /v1/videos and Sume POSTs a signed job envelope to it when the job reaches a terminal state. The URL must be HTTPS.
What is signed
When signing is configured, Sume signs the raw JSON body with HMAC SHA 256 over <timestamp>.<raw_body>. Two headers carry it: x-sume-webhook-timestamp and x-sume-webhook-signature, whose value looks like sume-v1=<hex>. During a secret rotation the signature header holds one entry per live secret, newest first, separated by commas, so accept the delivery if any sume-v1= entry matches.
Reject callbacks whose timestamp is outside your replay window. The docs suggest five minutes as a reasonable default. The signature must be computed over the raw body bytes, not over a parsed and re-serialized copy; a framework that parses JSON before your handler is the most common reason a correct secret fails.
| Header | Holds | Use |
|---|---|---|
| x-sume-webhook-timestamp | Unix seconds | Replay check and part of the signed string |
| x-sume-webhook-signature | sume-v1= entries, comma separated during rotation | Accept if any entry matches |
| x-sume-webhook-secret-fingerprint | Fingerprint of the signing secret | Compare with the dashboard when verification fails |
A verifier that refuses an empty secret
This Python function takes the raw body bytes. It returns false for an empty secret, a bad timestamp, a stale timestamp or no matching entry, and it compares every entry so timing does not reveal which one matched.
import hashlib, hmac, time
def verify(raw_body: bytes, timestamp: str, header: str, secret: str, tolerance: int = 300) -> bool:
if not secret:
return False
try:
ts = int(timestamp)
except ValueError:
return False
if abs(int(time.time()) - ts) > tolerance:
return False
signed = str(ts).encode() + b"." + raw_body
digest = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
expected = "sume-v1=" + digest
matched = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), expected):
matched = True
return matchedWhere the secret lives
The docs say to read the signing secret on the Webhooks tab of the dashboard, or from GET /v1/webhooks/signing-secret with an API key that carries account:read, and to store it as SUME_COM_WEBHOOK_SIGNING_SECRET. It is derived for your workspace. Job webhooks and run webhooks share it, so a single verifier covers both. Keep polling as a backup; a callback that fails to arrive or verify must not lose a finished clip.
A checklist when a callback is rejected
If all five pass and verification still fails, poll the job instead and send the request id to support.
- Compare the fingerprint header with the dashboard value.
- Confirm you hash the raw bytes, not a re-encoded copy.
- Confirm the string is the timestamp, a dot, then the body.
- During rotation, test each entry in the header, not only the first.
- Check the clock on your server against the five minute window.
Testing the verifier
Write three tests before you deploy. A correct body, timestamp and secret must pass. The same body with one byte changed must fail. An empty secret must fail even with a signature that would otherwise match, which is the case a configuration mistake produces in production. Add a fourth for rotation: a header with an old entry followed by the right one must pass.
The Python function above passes all four. Run it against a captured delivery from a development key before you trust it with a paid job.
Sources
Related posts
More in Developers
- Caption inputs on Sume: script_text, words, cues or segments?
Four caption inputs and only one may be sent. When to use script_text with transcription, word timings, or phrase cues for a silent clip. Plus the errors.
- video_filter_crop_out_of_bounds: FFmpeg crop pixels to fractions
Sume video-filter crop takes fractions of the frame, not pixels. Convert FFmpeg crop=w:h:x:y with a runnable Python check of the bounds.
- video_inspect 400s: frames at[] vs fps, and transcribe fields
Three video_inspect refusals: frames with both at[] and fps, frames with neither, and language_code without transcribe. Causes, fixes, a Python lint.
- A video provider interface after the Sora shutdown, Sume behind it
OpenAI lists the Sora API shutdown as 2026-09-24 with no replacement. Put video generation behind one interface so the next vendor exit is a config change.
Written by Sume