Signature validation ensures that webhooks are authentic and haven't been tampered with. It prevents unauthorized parties from sending fake webhooks to your endpoint.
Both you and the webhook provider share a secret key (never transmitted with webhooks)
Provider creates an HMAC hash of the payload using the secret key (typically SHA-256)
The signature is included in a header (e.g., X-Webhook-Signature)
Compute the same HMAC with your secret and compare - if they match, the webhook is authentic
This is the pattern used by Stripe, GitHub, Shopify, Razorpay and most providers. The secret key and encoding (hex vs base64) differs per provider — the structure does not.
const crypto = require('crypto');
const express = require('express');
const app = express();
// CRITICAL: Use express.raw() — NOT express.json() — for webhook routes.
// Once JSON middleware parses the body, the raw bytes are gone.
// All major providers (Stripe, GitHub, Razorpay) sign the raw bytes.
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const receivedSig = req.headers['x-webhook-signature'];
// req.body is a Buffer here — exactly what we need
const expectedSig = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body) // ← raw Buffer, NOT JSON.stringify(req.body)
.digest('hex'); // or 'base64' — depends on the provider
// Timing-safe comparison — prevents timing attacks
const sigBuffer = Buffer.from(receivedSig, 'hex');
const expBuffer = Buffer.from(expectedSig, 'hex');
if (sigBuffer.length !== expBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expBuffer)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Now safe to parse JSON
const event = JSON.parse(req.body);
console.log('Verified event:', event.type);
res.status(200).json({ received: true });
});
// ⚠️ Common mistake: using express.json() globally and then trying to
// reconstruct the raw body with JSON.stringify(req.body) — this WILL fail
// because JSON.stringify reorders keys and drops whitespace.| Provider | Header | Encoding | Strip Prefix? |
|---|---|---|---|
| Stripe | Stripe-Signature | hex (via SDK) | Use SDK only |
| GitHub | X-Hub-Signature-256 | hex | Yes — strip "sha256=" |
| Shopify | X-Shopify-Hmac-SHA256 | base64 | No prefix |
| Razorpay | X-Razorpay-Signature | hex | No prefix |
| Svix / Clerk | webhook-signature | base64 | Strip "whsec_" from secret |
| Twilio | X-Twilio-Signature | base64 (URL+params) | Use SDK — URL matters |
HookMetry Advantage:
HookMetry automatically validates signatures for Stripe, GitHub, and custom HMAC webhooks. You can see validation results in real-time, making debugging authentication issues effortless.
Was this page helpful?
Your feedback helps us improve the docs.