Complete guide to webhooks, debugging, and real-time event monitoring with HookMetry
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.