All Webhook Errors

Shopify Webhook HMAC Validation Failed — Causes & Fixes

Shopify signs webhook deliveries with HMAC-SHA256, encoded as base64 (not hex like Stripe and GitHub) in the X-Shopify-Hmac-Sha256 header. The base64 encoding is the most common reason teams' Stripe-shaped verification code fails when copy-pasted for Shopify.

Root Causes

1. Comparing hex against base64

Your verification code computes hmac.digest('hex') and compares against the header. Shopify's header is base64. They never match. Use hmac.digest('base64').

2. Body was parsed before HMAC was computed

Same root cause as Stripe and GitHub. Use express.raw() on the webhook route.

3. Wrong secret type

Shopify has two kinds of webhook secrets:

  • App webhooks (custom apps + public apps): signed with the app's API secret key (from Partner Dashboard → App → Configuration). All webhooks for all stores using that app share the same secret.
  • Admin-API webhooks (created via REST/GraphQL): signed with a per-shop secret returned in the create-webhook response.

Using the app secret to verify admin-API webhooks (or vice versa) produces 100% mismatches.

4. EU vs US data residency

Shopify's EU data residency (shopify.eu) signs differently in some legacy paths. If your store is on EU residency and your verification is failing, contact Shopify support to confirm which secret applies.

Fix It — Working Shopify Verification

import express from 'express'
import crypto from 'node:crypto'

const app = express()
const secret = process.env.SHOPIFY_WEBHOOK_SECRET

app.post('/webhooks/shopify',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-shopify-hmac-sha256']
    if (!sig) return res.status(401).send('missing signature')

    const expected = crypto
      .createHmac('sha256', secret)
      .update(req.body)
      .digest('base64')        // BASE64, not hex

    const sigBuf = Buffer.from(sig, 'utf-8')
    const expBuf = Buffer.from(expected, 'utf-8')
    if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
      return res.status(401).send('signature mismatch')
    }

    const payload = JSON.parse(req.body.toString())
    res.status(200).send('ok')
  }
)

Python Equivalent

import hmac, hashlib, base64
from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ['SHOPIFY_WEBHOOK_SECRET'].encode()

@app.post('/webhooks/shopify')
def shopify_webhook():
    sig = request.headers.get('X-Shopify-Hmac-Sha256', '')
    body = request.get_data()

    digest = hmac.new(SECRET, body, hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()

    if not hmac.compare_digest(sig, expected):
        return 'invalid signature', 401
    return 'ok', 200

Shopify-Specific Gotchas

  • Shopify retries up to 19 times across 48 hours. Idempotency store retention should cover at least 72 hours.
  • The X-Shopify-Webhook-Id header is your delivery ID. The X-Shopify-Topic header is the event type.
  • Shopify sends a X-Shopify-Test: true header on test webhooks fired from the admin. Don't confuse them with real events.
  • Shopify webhook timeouts are 5 seconds — much tighter than Stripe's 10s or GitHub's 10s. Your handler must be fast.

How to Reproduce

In Shopify Admin: Settings → Notifications → Webhooks → click your webhook → "Send test notification." Capture the request in WebhookWhisper, copy the body and signature, then verify locally. If your local verification works on the captured body but production doesn't, the prod body is being mutated upstream.

Frequently Asked Questions

Why is my hex-encoded signature failing for Shopify?

Shopify uses base64, not hex. This is the single biggest difference between Stripe/GitHub verification and Shopify verification. Switch your digest encoding.

Can I skip HMAC verification for Shopify if I'm using IP allowlists?

No. Shopify doesn't publish stable webhook IPs. HMAC is the only reliable authentication mechanism. Always verify.

My Shopify webhook signature works for some events but not others. Why?

Almost certainly different secrets. App-level webhooks use the app secret; admin-API webhooks use per-shop secrets. Check which type each webhook is and load the matching secret.

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
Shopify Webhook HMAC Validation Failed — Causes & Fixes (2026) | WebhookWhisper