The most frequent cause of 401 Unauthorized webhook failures in Node and TypeScript backends is verifying the HMAC-SHA256 signature against JSON.stringify(req.body) instead of the exact raw UTF-8 byte stream transmitted by Stripe or Shopify.
Whenever a global JSON body parser deserializes and re-serializes a payload, subtle differences in key spacing, escaped characters, or scientific notation produce a completely different SHA-256 digest.
import crypto from 'node:crypto'; export function verifyStripeWebhookRawHmac({ rawBodyBuffer, sigHeader, secret, nowSec, toleranceSec = 300 }) { const parts = Object.fromEntries( String(sigHeader || '').split(',').map((kv) => kv.trim().split('=')) ); const ts = Number(parts.t); if (!Number.isInteger(ts) || Math.abs(nowSec - ts) > toleranceSec) return false; const signedPayload = `${ts}.${rawBodyBuffer.toString('utf8')}`; const expected = crypto.createHmac('sha256', secret).update(signedPayload, 'utf8').digest(); const provided = Buffer.from(String(parts.v1 || ''), 'hex'); return expected.length === provided.length && crypto.timingSafeEqual(expected, provided); } - Engineering deep-dive: Stripe & Shopify Webhook HMAC Raw-Body Pitfalls
- Interactive sandbox: Free Stripe & Shopify Webhook HMAC Signature Tester