All Webhook Errors

Webhook Signature Mismatch — Causes & Fixes

A webhook signature mismatch means the HMAC value you computed from the payload does not equal the value the provider sent in the signature header. The provider sent the correct signature. The mismatch is always on the receiver side.

The Five Root Causes

1. Using the parsed body instead of the raw body

HMAC is computed over the raw bytes of the request body — the exact sequence of characters as transmitted. When you let a JSON parser touch the body first, it reformats whitespace, reorders keys, or normalizes encoding. The byte sequence changes and the HMAC will never match.

// Node.js / Express — always use express.raw() for webhook routes
app.post('/webhook', express.raw({ type: 'application/json' }), handler)

// Django — access request.body (raw bytes) not request.data
raw_body = request.body  # correct
payload = json.loads(raw_body)  # parse after verification

2. Wrong webhook secret

Webhook secrets are per-endpoint. If you register multiple webhook URLs, each gets a different secret. Using the secret from endpoint A to verify deliveries to endpoint B always mismatches.

3. Wrong encoding

Stripe and GitHub use hex encoding. Shopify uses base64. Comparing a hex string to a base64 string always fails. Check the provider docs for the exact encoding.

4. Wrong algorithm

Most providers use HMAC-SHA256. Twilio uses HMAC-SHA1. Using sha1 where sha256 is expected always mismatches.

5. Extra whitespace or BOM in the secret

If you copy-paste the secret and accidentally include a trailing newline or space, the HMAC will never match. Always trim: process.env.WEBHOOK_SECRET.trim().

Algorithm and Encoding by Provider

ProviderHeaderAlgorithmEncoding
StripeStripe-SignatureHMAC-SHA256Hex (with timestamp prefix)
GitHubX-Hub-Signature-256HMAC-SHA256Hex with sha256= prefix
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256Base64
TwilioX-Twilio-SignatureHMAC-SHA1Base64
WooCommerceX-WC-Webhook-SignatureHMAC-SHA256Base64
SendGridX-Twilio-Email-Event-Webhook-SignatureECDSA-SHA256Base64

Node.js Generic Verifier

const crypto = require('crypto')

function verifyWebhookSignature(rawBody, headerSig, secret, algorithm, encoding) {
  const hmac = crypto.createHmac(algorithm, secret.trim())
  hmac.update(rawBody)
  const computed = hmac.digest(encoding)
  return crypto.timingSafeEqual(Buffer.from(headerSig), Buffer.from(computed))
}

Python Generic Verifier

import hashlib, hmac, base64

def verify_signature(raw_body, header_sig, secret, algorithm='sha256', encoding='hex'):
    key = secret.strip().encode()
    h = hmac.new(key, raw_body, getattr(hashlib, algorithm))
    computed = base64.b64encode(h.digest()).decode() if encoding == 'base64' else h.hexdigest()
    return hmac.compare_digest(header_sig, computed)

FAQ

The signature matched yesterday but fails today — what changed?

Most likely the webhook secret was rotated in the provider dashboard. Check for a new secret and update your environment variable. Some providers like Stripe allow a tolerance window during rotation where both old and new secrets are accepted.

How can I log the computed HMAC to compare with the header?

Temporarily add console.log('computed', computed, 'header', headerSig). If computed is empty or undefined, the raw body is not reaching the verifier. If lengths differ, check the encoding.

Does WebhookWhisper show the raw body?

Yes. WebhookWhisper captures the exact raw bytes of every incoming request. Copy the raw body from the inspector, paste it into a local script with your secret, and compute the expected HMAC to confirm the mismatch before deploying your fix.

Debug This Error in Real Time

WebhookWhisper captures every webhook request with full headers, body, and timing — so you can see exactly what the provider sent and reproduce the error instantly.

Start Debugging Free
Webhook Signature Mismatch — Causes & Fixes (2026) | WebhookWhisper