Replay protection and tolerance
t is the time of each attempt, and every retry is signed again with a fresh t, so a retry hours later still verifies. A captured request replayed after the tolerance window fails with timestamp_out_of_tolerance. Within the window, reject repeats by remembering the event id. Keep your server clock synchronized (NTP); the check compares your clock with ours in both directions.
Several v1= values in one header are accepted if any matches. Other languages can verify with the formula below and a constant-time comparison.
signed_payload = t + "." + raw_body
expected = hex(hmac_sha256(callback_secret, signed_payload))
valid = constant_time_equal(expected, v1) and abs(now - t) <= 300
A tampered body and a stale timestamp
import { createHmac } from "node:crypto";
import { verifyWebhook, WebhookSignatureError } from "@img2site/sdk-server";
const secret = "whsec_test";
const body = JSON.stringify({
id: "upload.ready:upl_test",
event: "upload.ready",
created_at: new Date().toISOString(),
data: { file_slug: "hero", revision: 1 },
});
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const headers = { "x-img2site-signature": `t=${t},v1=${v1}` };
verifyWebhook(body, headers, secret); // → { id, event: "upload.ready", created_at, data }
try {
verifyWebhook(body.replace('"hero"', '"admin"'), headers, secret);
} catch (error) {
console.log((error as WebhookSignatureError).reason); // "signature_mismatch"
}
try {
// The same delivery replayed ten minutes later.
verifyWebhook(body, headers, secret, { now: new Date((t + 600) * 1000) });
} catch (error) {
console.log((error as WebhookSignatureError).reason); // "timestamp_out_of_tolerance"
}