Webhook Debugging Guide
Webhook timestamp expired when replaying a captured event
Short answer
You saved a webhook payload and its headers, and posting it back to your own server later fails signature verification, even though the secret is correct. That's expected: Stripe, Slack and Polar all sign a timestamp along with the body, and reject anything older than a tolerance window, usually about 5 minutes, to stop exactly this kind of replay. The fix is to compute a new signature with the current timestamp before resending, not to widen the tolerance in production.
Which providers this applies to
- Stripe:
t=inStripe-Signature, 300-second tolerance. - Slack:
X-Slack-Request-Timestamp, checked against the signature separately fromX-Slack-Signature, 300 seconds. - Polar (Standard Webhooks):
webhook-timestamp, roughly 300 seconds. - GitHub and Shopify: no timestamp in the signed content at all. A saved payload verifies the same regardless of age.
Re-signing by hand: Stripe example
Stripe's signed content is {timestamp}.{raw body}, HMAC-SHA256 hex-encoded:
const crypto = require('crypto');
function resignStripe(body, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
Set that as the Stripe-Signature header on the replayed request, alongside the original, unmodified body. Slack's v0:{timestamp}:{body} and Polar's Standard Webhooks {id}.{timestamp}.{body} follow the same shape with a different secret format and header layout.
What doesn't need this
GitHub's X-Hub-Signature-256 and Shopify's X-Shopify-Hmac-Sha256 are computed over the body alone, so a saved payload from any point in the past replays cleanly — the age of the event has never been part of what's verified for those two.
The easier way: WebhookMon
WebhookMon already holds your endpoint's secret, so its Replay button can re-sign with the current timestamp automatically for Stripe, Polar and Slack — one click instead of a script you rewrite each time you need it. GitHub and Shopify replay byte for byte, since re-signing wouldn't change anything for them.