Signature Verification
Signature Verification
Most webhook providers sign their payloads so you can verify the request actually came from them. This guide covers the verification approach per provider.
Important: Always verify signatures against the raw request body, not a re-serialized JSON object. Re-serialization changes whitespace and key order, causing verification failures.
Stripe
Stripe signs using HMAC-SHA256. The signature is in the Stripe-Signature header. The signed payload is {timestamp}.{rawBody}.
node.js
// Node.js (stripe npm package)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const sig = request.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
rawBody, // raw Buffer, not parsed JSON
sig,
process.env.STRIPE_WEBHOOK_SECRET // whsec_...
);
Common failure: using req.body (already-parsed JSON) instead of the raw bytes.
GitHub
GitHub signs using HMAC-SHA256. The signature is in X-Hub-Signature-256 with format sha256=<hex>.
node.js
// Node.js
const crypto = require('crypto');
function verifyGitHub(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
const valid = verifyGitHub(
rawBody,
req.headers['x-hub-signature-256'],
process.env.GITHUB_WEBHOOK_SECRET
);
Shopify
Shopify signs using HMAC-SHA256. The Base64-encoded signature is in X-Shopify-Hmac-Sha256.
node.js
// Node.js
const crypto = require('crypto');
function verifyShopify(rawBody, signature, secret) {
const digest = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
const valid = verifyShopify(
rawBody,
req.headers['x-shopify-hmac-sha256'],
process.env.SHOPIFY_WEBHOOK_SECRET
);
Slack
Slack signs using HMAC-SHA256. Build the base string as v0:{timestamp}:{rawBody} and compare against X-Slack-Signature.
node.js
// Node.js
const crypto = require('crypto');
function verifySlack(rawBody, timestamp, signature, secret) {
// Reject if timestamp is more than 5 minutes old (replay protection)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const base = `v0:${timestamp}:${rawBody}`;
const expected = 'v0=' + crypto
.createHmac('sha256', secret)
.update(base)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
The Anonymily dashboard (Pro) detects the provider automatically and shows a verification result + failure reason for each captured request.