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.

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.
| Step | Use |
|---|---|
| Signed string | The header text as received |
| Freshness check | Number(header) against Math.floor(Date.now() / 1000) |
| Tolerance | 300 seconds by default |
| Boundary | 300 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
- YouTube thumbnail test set: n=4 on GPT Image 2.5 high may return 202
Four 1280x720 thumbnail takes cost $0.1425 on GPT Image 2.5 high via Sume, and a slow call can return 202 instead of 200. A Python client that handles both.
- Zed context_servers for the hosted Sume server: no header means OAuth
Add the hosted Sume server to Zed's settings.json context_servers with a url. With no Authorization header Zed runs the MCP OAuth flow, so start with mcp:read.
- Which MCP server lets Claude Code or Cursor generate video and images?
MCP servers that let Claude Code and Cursor make video and images: Sume, fal, Replicate, Runway, Higgsfield. Endpoints, sign-in, billing, setup.
- Idempotency keys for AI video APIs: retry without paying twice
An idempotency key makes a retried create return the original run or job instead of a second paid one. How Sume's Idempotency-Key works on each API.
Written by Sume