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.

6 min readSume
All posts

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.

Headers on a Sume job webhook (read 2026-10-03)
HeaderHoldsUse
x-sume-webhook-timestampUnix secondsReplay check and part of the signed string
x-sume-webhook-signaturesume-v1= entries, comma separated during rotationAccept if any entry matches
x-sume-webhook-secret-fingerprintFingerprint of the signing secretCompare 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 matched

Where 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

All Developers posts

Written by Sume