All Webhook Errors

Webhook 401 Unauthorized Error — Causes & Fixes

A webhook 401 Unauthorized response means your endpoint rejected the request because the signature verification failed — the HMAC computed from the incoming payload does not match the value in the signature header. This is one of the most common webhook errors, and almost always comes down to one of three root causes.

Root Causes

1. Parsing the body before verifying the signature

HMAC verification requires the raw request bytes — exactly as sent by the provider. If your framework parses the JSON body before you read the raw bytes, the byte sequence changes (whitespace, key order, encoding) and the HMAC will never match. This is the most common cause of 401 errors on Stripe, GitHub, and Shopify webhooks.

// WRONG — body is already parsed, HMAC will fail
app.use(express.json())
app.post('/webhook', (req, res) => {
  const sig = req.headers['stripe-signature']
  stripe.webhooks.constructEvent(req.body, sig, secret) // fails
})

// CORRECT — use raw body for the webhook route only
app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['stripe-signature']
    const event = stripe.webhooks.constructEvent(req.body, sig, secret) // works
    res.json({ received: true })
  }
)

2. Wrong webhook secret

Each webhook endpoint gets its own signing secret. If you rotate the secret in the provider dashboard but do not update your environment variable, every delivery will return 401. Check that WEBHOOK_SECRET in your deployment matches the current secret shown in the provider dashboard.

3. Charset or encoding mismatch

Some providers send the signature as hex, others as base64. Check the provider docs for the exact encoding used in the header.

Fix by Provider

ProviderSignature HeaderAlgorithmKey Issue
StripeStripe-SignatureHMAC-SHA256Must use raw body; use stripe.webhooks.constructEvent()
GitHubX-Hub-Signature-256HMAC-SHA256Compare hex digest, strip sha256= prefix
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256Header is base64-encoded; use raw body
TwilioX-Twilio-SignatureHMAC-SHA1Sorts params alphabetically before hashing

Node.js Fix (Express)

const crypto = require('crypto')
const express = require('express')
const app = express()

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const secret = process.env.WEBHOOK_SECRET
    const headerSig = req.headers['x-hub-signature-256'] || ''
    const computed = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(req.body)
      .digest('hex')

    if (!crypto.timingSafeEqual(Buffer.from(headerSig), Buffer.from(computed))) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    const event = JSON.parse(req.body)
    res.json({ received: true })
  }
)

Python Fix (FastAPI)

import hashlib, hmac, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post('/webhook')
async def webhook(request: Request):
    secret = os.environ['WEBHOOK_SECRET'].encode()
    raw_body = await request.body()
    header_sig = request.headers.get('x-hub-signature-256', '')
    expected = 'sha256=' + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(header_sig, expected):
        raise HTTPException(status_code=401, detail='Invalid signature')
    return {'received': True}

FAQ

Why does HMAC fail only in production but work locally?

Your local server likely uses a different body parser configuration. Check whether a global express.json() middleware runs before your webhook route in production. Move the webhook route before any body-parsing middleware, or use express.raw() specifically for that route.

Can I test HMAC verification without a real provider?

Yes. WebhookWhisper lets you send test payloads with a custom HMAC signature using any secret. Set the secret in your handler, fire a test payload from the provider testing page, and verify the signature in your logs.

What is timingSafeEqual and why does it matter?

crypto.timingSafeEqual compares two buffers in constant time, preventing timing attacks where an attacker could guess the signature one byte at a time by measuring response latency. Always use it instead of === for signature comparison.

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 401 Unauthorized Error — Causes & Fixes (2026) | WebhookWhisper