Sume webhook signature fails: check the secret fingerprint first

When a Sume webhook signature will not verify, compare the 12-character secret fingerprint header before you debug the HMAC. Includes a Python verifier.

4 min readSume
All posts

If a Sume webhook signature will not verify, first compare the x-sume-webhook-secret-fingerprint header with the fingerprint shown next to your signing secret in the dashboard. The header is 12 hex characters. If the two differ, your receiver holds a different secret from the one that signed the delivery, and no amount of HMAC debugging will fix that. Neither side ever has to send the secret itself.

Where the fingerprint appears

Each delivery carries three headers: a timestamp, a signature of the form sume-v1=<hex>, and the fingerprint. The signature is HMAC-SHA256 over <timestamp>.<raw_body> with your workspace's signing secret, computed on the raw bytes before parsing. The same fingerprint is on the run receipt as webhook_delivery.signing_secret_fingerprint.

Webhook verification facts, read 2026-10-08
ItemRule
Signature headerx-sume-webhook-signature: sume-v1=<hex hmac-sha256>
Signed text<timestamp>.<raw_body>
Timestamp windowReject outside five minutes
Fingerprint headerx-sume-webhook-secret-fingerprint, 12 hex characters
Secret sourceDashboard Webhooks tab, or GET /v1/webhooks/signing-secret with account:read
Env name used by SumeSUME_COM_WEBHOOK_SIGNING_SECRET

A verifier that refuses an empty secret

The function below checks the window, refuses an empty secret, and compares against every comma-separated signature entry, which keeps it working while a secret is being rotated. The demo at the bottom signs a sample body and verifies it.

import hashlib, hmac, time

def verify(secret, ts, raw, header):
    if not secret:
        raise ValueError("empty webhook secret")
    if abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    want = "sume-v1=" + mac.hexdigest()
    return any(hmac.compare_digest(p.strip(), want) for p in header.split(","))

raw = b'{"event":"format.run.terminal"}'
ts = str(int(time.time()))
sig = "sume-v1=" + hmac.new(b"s3cret", ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
print(verify("s3cret", ts, raw, sig))

Steps when it fails

Work from the cheapest check to the dearest.

  • Compare the fingerprint header with the dashboard fingerprint. A mismatch means the wrong secret.
  • Confirm you verify the raw request bytes, not a re-serialized JSON body.
  • Check your server clock against the five-minute window.
  • Check that the environment variable is set and not empty before the process starts.

What Sume does not do

Sume does not send the secret in any delivery, and the fingerprint is not a secret you can verify with; it only tells you which secret was used. A failed verification on your side should return a non-2xx answer, and Sume will then count the attempt as failed and retry within its documented limits.

Secret rotation and retries

During a signing-secret rotation the signature header carries one entry for each live secret, newest first, separated by commas, and the receiver should accept the delivery if any entry verifies. The verifier above does that. Without it, a receiver that reads the header as a single value will fail during the rotation window even though nothing is wrong with the secret.

Sume treats any non-2xx answer, and any answer that takes more than 10 seconds, as a failed attempt. It tries up to 10 times, with a backoff that grows from 30 seconds with jitter up to one hour, or longer if you send a Retry-After on a 429 or 503. A delivery outcome never changes the run, so a verification bug on your side leaves the run completed and the delivery status failed or exhausted. You can ask for a new delivery with the redeliver endpoint once the receiver is fixed.

Sources

Related posts

More in Formats

All Formats posts

Written by Sume