Never set toleranceSeconds to 0 on a Sume webhook verifier
toleranceSeconds 0 skips the timestamp check, so a captured delivery verifies forever. See the replay and a WebCrypto verifier that rejects stale ones.

Setting toleranceSeconds: 0 in the Sume SDK's verifyWebhook turns off the timestamp check, so a signed delivery that an attacker or a log leak captured once will verify again next week. The signature proves the body and the timestamp were signed with your secret; only the tolerance window proves the delivery is recent. Sume SDK webhooks documents the default as 300 seconds and states that 0 skips the check, which is useful in a test that replays a fixture and dangerous anywhere else.
The mistake usually arrives as a fix for a failing test: a fixture with a fixed timestamp stops verifying, someone adds a zero, and the zero ships. A one-line lint or a test that asserts the production config never contains it is cheap insurance.
What does a replay actually do?
Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. A replay sends the same three things again. Without a window, your handler sees a valid signature and a valid body, and runs the job.completed logic a second time: a second publish, a second email, a second charge to your customer. Deduping on job_id is the second line of defence, and you want both, because dedupe state can be lost and a clock check cannot.
How do I verify with a window and no SDK?
const enc = new TextEncoder();
const hex = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
async function verify({ body, ts, header, secret, tolerance = 300, now = Date.now() }) {
if (!secret) throw new Error("empty signing secret: refusing to verify");
const t = Number(ts);
if (!Number.isFinite(t) || Math.abs(Math.floor(now / 1000) - t) > tolerance) return false;
const key = await crypto.subtle.importKey("raw", enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = hex(await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${body}`)));
return header.split(",").some((p) => p.trim() === `sume-v1=${mac}`);
}
(async () => {
const body = '{"event":"job.completed"}', secret = "whsec_test";
const now = Date.now(), ts = Math.floor(now / 1000) - 900;
const key = await crypto.subtle.importKey("raw", enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = hex(await crypto.subtle.sign("HMAC", key, enc.encode(`${ts}.${body}`)));
const header = `sume-v1=${mac}`;
console.log("15 min old, 300 s window:", await verify({ body, ts, header, secret, now }));
console.log("same delivery, window off:", await verify({ body, ts, header, secret, now, tolerance: Infinity }));
})();What does the output show?
Run it and the first line prints false while the second prints true, which is exactly the behaviour of a zero tolerance: the old delivery is accepted. The code also splits the header on commas and accepts any sume-v1= entry, because during the 24 hour rotation window Sume sends one signature per live secret, newest first. And it refuses an empty secret outright, since HMAC with an empty key is valid and would let anyone who knows the scheme forge a delivery.
| Setting | Effect | Use it |
|---|---|---|
| 300 (default) | Rejects deliveries older or newer than 5 minutes | Production |
| Smaller, such as 60 | Tighter, needs good clock sync | If your hosts run NTP |
| 0 | Skips the timestamp check | Replaying a fixture in a test only |
What does the SDK check, and in what order?
The Sume SDK's verifyWebhook is async because it uses WebCrypto rather than node:crypto, so you must await it, and it returns false rather than throwing on a malformed delivery: a missing header, a bad timestamp and a wrong signature all end in the same failed verification. It enforces the replay window before it computes the HMAC and compares in constant time. Its input is the raw body, the headers, your signing secret and an optional toleranceSeconds (default 300).
That ordering is why a tolerance of zero is worse than a loose window. A 300 second window rejects anything older or newer than five minutes before any signature work. A zero skips that gate entirely and leaves only the signature, which a captured delivery already satisfies. If you hand-roll the check instead, as in the sample above, keep the same order: parse the timestamp, compare it to the clock, then compute and compare the HMAC.
The same scheme covers both Sume webhook surfaces. Generation-job webhooks (job.completed, job.failed) and Format run webhooks use the same sume-v1 signature, so one verifier and one tolerance setting protect both. Route on the event field after you have verified, and answer an unrecognized event with a 204 so a new event type does not turn into a retry storm.
How do I debug stale-timestamp rejections?
Keep your server clock honest. A verifier with a 300 second window fails every delivery if the host clock drifts past it, and the symptom looks like a bad secret. Compare x-sume-webhook-secret-fingerprint to the dashboard fingerprint first, and check the clock second, because the two failures look identical in a log that only says false. If you need to replay a real delivery, use the redeliver endpoint, which re-posts the terminal event with a fresh timestamp and signature, so you never need a zero. If a test needs a frozen timestamp, pass the now value into the verifier, as the sample above does, instead of loosening the window: the test stays deterministic and production keeps its protection.
Sources
Related posts
More in Developers
- A Node CLI to submit a Sume video job: util.parseArgs and --dry-run
A Node script with util.parseArgs that validates duration, builds the /v1/videos request, and prints it with --dry-run before anything bills. Tested offline.
- Node fetch to Sume with Ideogram 4.5: branch on status 200 or 202
POST /v1/images returns 200 with data[].url or 202 with a job envelope. A Node 22 fetch sample that checks res.status, with a 40 second abort signal.
- Node fetch worker pool: submit transcription jobs with retry-after
A 30-line Node 18+ worker pool that posts Sume STT jobs, sleeps for retry-after on 429, and sends an Idempotency-Key per clip. Tested against a stub.
- Nova Canvas boto3 read timeout vs Sume's 30 second wait and 202
The AWS SDK read timeout is 60 s and Amazon suggests 300 s for Nova Canvas. Sume's /v1/images waits 30 s, then returns a 202 job. Handle both in Python.
Written by Sume