x-sume-webhook-timestamp is Unix seconds: the Date.now() mistake

Sume's x-sume-webhook-timestamp is Unix seconds. Comparing it to Date.now() in Node is off by 1000 and rejects every delivery. A short, tested fix.

3 min readSume
All posts

The x-sume-webhook-timestamp header holds Unix time in seconds, and Sume produces it as Math.floor(Date.now() / 1000). If your Node receiver compares it to Date.now() directly, the difference is about a thousand times too large, and every delivery looks stale.

The fix is one line: convert your own clock to seconds before comparing, then apply the 300 second tolerance.

The two comparisons

The sample builds a header value five seconds old, then checks it both ways. It printed true for the seconds comparison and false for the milliseconds mistake.

A related trap is reading the header as a string and adding to it. In JavaScript, '1780000000' + 300 is a string concatenation, not a sum, so convert with Number first as the sample does. Another is using the parsed value in the signed string: use the original header text there, because converting and printing it back can change formatting if the header ever carries something unusual. Keep the signed string, the freshness check and the tolerance in three clearly separate lines of code and the unit bug has nowhere to hide.

const sent = String(Math.floor(Date.now() / 1000) - 5);   // header value, Unix seconds
const nowSec = Math.floor(Date.now() / 1000);
console.log("seconds ok:", Math.abs(nowSec - Number(sent)) <= 300);
console.log("ms mistake:", Math.abs(Date.now() - Number(sent)) <= 300);

Where the unit also matters

The timestamp is part of the signed string, which is the timestamp, a dot and the raw body. Use the header value exactly as received when you compute the digest; do not reformat or convert it there. Convert only for the freshness check.

Timestamp handling (read 2026-10-06, Sume docs)
StepUse
Signed stringThe header text as received
Freshness checkNumber(header) against Math.floor(Date.now() / 1000)
Tolerance300 seconds by default
Boundary300 passes, 301 fails

Symptoms of the bug

Every real delivery fails with your own stale-timestamp error while your signature test with a hand-made header passes, because you built that header with the same wrong unit. Check that your test fixtures use seconds too.

Because Sume re-signs each retry with a fresh timestamp, a correct receiver will accept a retry even after an earlier attempt was rejected.

Tradeoffs

A 300 second window assumes your clock is accurate. Run NTP on the host. A wider window is more forgiving and also gives replays more room, so widen it only when you have to.

If you cannot tell which unit a log shows, count digits: a Unix seconds value is ten digits today, and a milliseconds value is thirteen. That quick check settles most arguments about why a freshness test keeps failing.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume