verifyWebhook toleranceSeconds: 0 turns the replay window off

In @sume-com/sdk, toleranceSeconds: 0 skips the timestamp check, so a day-old signed delivery verifies. Keep the 300-second default.

4 min readSume
All posts

The verifyWebhook helper in @sume-com/sdk returns true only when two things hold: the signature matches and the timestamp is inside a replay window. The window defaults to 300 seconds, the same figure the webhook guide recommends. The option that sets it, toleranceSeconds, has one value that is easy to misread.

Passing 0 does not mean a zero-second window that rejects everything. The check is written as toleranceSeconds > 0 && ..., so 0 skips the timestamp comparison altogether. The signature is still checked, but a captured delivery now verifies forever.

What changes with the option

verifyWebhook outcomes for a signed delivery that is one day old, run on @sume-com/sdk 0.2.0 (read 2026-10-03)
CallResult
Default tolerance (300 seconds)false
toleranceSeconds: 0true
toleranceSeconds: 0 with the wrong secretfalse

Reproduce it

This script signs a body with a timestamp from 24 hours ago, the way a delivery would be signed, and calls the verifier twice. Run it as an ES module with Node 22 after npm install @sume-com/sdk. The top-level await works because the file is a .mjs module.

import { createHmac } from "node:crypto";
import { verifyWebhook } from "@sume-com/sdk";

const secret = "whsec_test";
const body = '{"event":"job.completed","job_id":"job_1"}';
const ts = Math.floor(Date.now() / 1000) - 86_400; // a day old
const sig =
  "sume-v1=" + createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
const headers = {
  "x-sume-webhook-timestamp": String(ts),
  "x-sume-webhook-signature": sig,
};

console.log("default:", await verifyWebhook({ body, headers, secret }));
console.log(
  "toleranceSeconds 0:",
  await verifyWebhook({ body, headers, secret, toleranceSeconds: 0 }),
);

Why the window matters

  • The signature covers the timestamp and the raw body together, so an attacker cannot change either. They can still resend an exact copy. The window limits how long a stolen copy is useful.
  • With the check off, only your own dedupe stands in the way. Sume tells receivers to use job_id as the idempotency key, and a replayed old event that your table has forgotten would be processed again.
  • Tests are the usual reason people reach for 0. Pass a now function that returns the delivery's timestamp instead, which keeps the window on and the fixture valid.
  • The helper never throws on a bad delivery. A missing header, a garbage timestamp or an empty secret all return false, so one if in the handler is enough, and the handler should answer 401 for it.
  • The same verifier covers job webhooks and run webhooks, so the window you choose applies to both kinds of delivery.
  • If your clock drifts, fix the clock or widen the window to a few minutes. Do not remove it.

The recommended window and the signing scheme are in the webhooks guide. The package itself is on npm.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume