Rolling a webhook secret: Stripe's 24-hour overlap vs Sume's header

Stripe can keep an old signing secret live for up to 24 hours. Sume sends one sume-v1 entry per live secret; verify any match. Python verifier included.

5 min readSume
All posts

To rotate a webhook signing secret without dropping events, verify against every signature in the header and accept the delivery if any one matches. Stripe's guide describes rolling a secret with an optional delay of up to 24 hours, during which it generates one signature per active secret. Sume's job and run webhooks work the same way: during a rotation the x-sume-webhook-signature header carries one sume-v1= entry per live secret, newest first, separated by commas.

What the header looks like

Sume signs <timestamp>.<raw_body> with HMAC SHA-256, and the timestamp arrives in x-sume-webhook-timestamp. In a rotation the header reads sume-v1=<new>,sume-v1=<previous>. A verifier that only reads the first entry works until the day you rotate, and then your old deployment (still holding the previous secret) rejects every delivery. Compare against all entries.

A verifier that survives rotation

The sketch refuses an empty secret, enforces the five-minute replay window, and compares every entry in constant time.

import hashlib, hmac, time

def verify(raw: bytes, ts: str, header: str, secret: str, tol: int = 300) -> bool:
    if not secret:
        raise ValueError("empty signing secret")
    try:
        age = abs(time.time() - int(ts))
    except ValueError:
        return False
    if age > tol:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    want = "sume-v1=" + mac.hexdigest()
    ok = False
    for part in header.split(","):
        if hmac.compare_digest(part.strip(), want):
            ok = True
    return ok

ts = str(int(time.time()))
body = b'{"event":"job.completed"}'
sig = "sume-v1=" + hmac.new(b"new", ts.encode() + b"." + body, hashlib.sha256).hexdigest()
print(verify(body, ts, "sume-v1=bad," + sig, "new"))  # True

Rotation checklist

Secret rotation facts (read 2026-10-05)
ItemStripeSume
OverlapOld secret can stay active up to 24 hoursHeader carries one entry per live secret
Header formatStripe-Signature with t= and v1=x-sume-webhook-signature: sume-v1=<hex>
Replay toleranceLibraries default to 5 minutesReject outside your window; 5 minutes suggested
Debug a mismatchTroubleshooting guideCompare x-sume-webhook-secret-fingerprint with the dashboard

Two reminders

Verify against the raw body, byte for byte. Any framework that parses and re-serializes JSON first breaks the signature, a point Stripe's guide stresses too. And read the secret from the dashboard Webhooks tab or GET /v1/webhooks/signing-secret; it is derived per workspace and shared by job and run webhooks, so one verifier covers both.

Rehearsing a rotation

Rotation fails in production for boring reasons: two services read the secret from different environment variables, a cache holds the old value, or the verifier trims the header at the first comma. Rehearse it. Send a test delivery from the dashboard or POST /v1/webhooks/test-deliveries (scope account:write) to a staging URL before and after the rotation. The test body is a dummy webhook.test payload with no job_id, so it exercises signature verification without touching real work.

If a signature does not verify after a rotation, compare fingerprints rather than secrets. Each delivery carries x-sume-webhook-secret-fingerprint, and the dashboard shows the fingerprint next to the secret. A mismatch means your service holds a different secret than the one signing. Neither side ever needs to paste the secret into a ticket.

Store the secret as SUME_COM_WEBHOOK_SIGNING_SECRET, the name the delivery worker uses, so the variable has one obvious home across services.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume