Clock changes vs clock drift: what breaks Sume webhook checks

Sume webhook timestamps are Unix seconds, so time zone changes cannot break the 5-minute replay check; a drifting server clock can. A check to tell them apart.

4 min readSume
All posts

No: a daylight saving change, or a tz database update, cannot make a valid Sume webhook look stale. The x-sume-webhook-timestamp header is a plain Unix time in seconds (the docs show a value like 1780000000), which has no time zone. What can break the replay check is your server's clock being wrong by more than the tolerance, which is five minutes in the webhook docs and 300 seconds by default in the SDK's `verifyWebhook`.

The question comes up every autumn when clocks change, and this week Node.js 26.11.0 shipped with an updated time zone database (tz 2026e, per the release notes). Neither touches Unix seconds. A container or VM whose clock has drifted does, usually after a pause, a snapshot restore or a missing time sync.

Tell the two apart in your logs

A failed check can mean a wrong secret, a changed body or a stale timestamp, and they need different fixes. The usual mistake is a single return False that hides which one it was. Verify the signature first, then compute the skew and report how far outside the window the delivery was. If many deliveries are stale by nearly the same amount, your clock is off by that amount. If only some are, a queue or proxy in front of the receiver is holding requests.

Webhook timestamp facts and what they imply (read 2026-10-08)
FactValueSource
Timestamp headerx-sume-webhook-timestamp, Unix secondsSume webhook docs
Signed string<timestamp>.<raw_body>, HMAC-SHA256Sume webhook docs
Replay window5 minutes suggested, SDK default 300 sSume webhook and SDK docs
SDK toleranceSeconds 0Skips the timestamp checkSume SDK docs
RedeliverSends a fresh timestamp and signatureSume webhook docs

Steps

  • Check the host clock first: compare date -u +%s on the receiver with a trusted source and enable time sync on the VM or container host.
  • Alert on the skew, not on single failures: one stale delivery is noise, a steady stream of them is a clock.
  • Log the signature result and the skew in seconds as separate fields.
  • Do not widen the tolerance to hide a bad clock. A window of hours turns replay protection off in practice.
  • For a delivery that was rejected only for being stale, use POST /v1/jobs/{job_id}/webhook/redeliver after fixing the clock; it needs the jobs:write scope and sends a fresh timestamp.

Checker

This function verifies the signature before reading the timestamp and returns ok, bad_signature or stale_by_<n>s. It refuses to start with an empty secret. The test block signs three bodies at ages 0, 400 and minus 400 seconds.

import hashlib
import hmac
import os
import time

SECRET = os.environ.get("SUME_COM_WEBHOOK_SIGNING_SECRET", "")
if not SECRET:
    raise SystemExit("SUME_COM_WEBHOOK_SIGNING_SECRET is empty")


def check(raw: bytes, ts: str, sig_header: str, tolerance: int = 300) -> str:
    want = hmac.new(SECRET.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    parts = [p.strip().removeprefix("sume-v1=") for p in sig_header.split(",")]
    if not any(hmac.compare_digest(p, want) for p in parts):
        return "bad_signature"
    skew = time.time() - int(ts)
    return "ok" if abs(skew) <= tolerance else f"stale_by_{abs(skew) - tolerance:.0f}s"


if __name__ == "__main__":
    body = b'{"event":"job.completed"}'
    for age in (0, 400, -400):
        ts = str(int(time.time()) - age)
        sig = hmac.new(SECRET.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
        print(age, check(body, ts, f"sume-v1={sig}"))

What Sume does not do

Sume does not adjust timestamps for your clock and does not offer a clock-skew allowance beyond the window you configure. Setting the SDK tolerance to 0 skips the timestamp check entirely, which the SDK docs list as an option. Keep job-level polling as a backup for events you reject.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume