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.