48-hour launch: 40% off with code LAUNCH40, Guide $5.40 / Kit $11.40

Shopify webhook HMAC verification failing: how to fix X-Shopify-Hmac-Sha256 mismatches

Shopify signs each webhook with HMAC-SHA256 over the raw request body, using your app's secret, and sends it base64-encoded in the X-Shopify-Hmac-Sha256 header. A mismatch almost always comes from one of these:

  1. The body was parsed first. Re-serialized JSON isn't byte-identical. Hash the raw bytes.
  2. Comparing hex to base64. Shopify's header is base64. Compute .digest('base64'), not 'hex'.
  3. Wrong secret. Use the app's client secret (API secret key), not the API key or an access token. Webhooks created under Settings, Notifications in the Shopify admin are signed with the key shown on that page.
  4. Non-constant-time compare isn't what breaks it, but you should still use timingSafeEqual / hmac.compare_digest.

Express

app.post('/shopify', express.raw({ type: 'application/json' }), (req, res) => {
  const digest = crypto.createHmac('sha256', process.env.SHOPIFY_SECRET).update(req.body).digest('base64');
  const header = req.get('X-Shopify-Hmac-Sha256') || '';
  const ok = header.length === digest.length && crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(header));
  res.sendStatus(ok ? 200 : 401);
});

Flask

digest = base64.b64encode(hmac.new(secret.encode(), request.get_data(), hashlib.sha256).digest()).decode()
ok = hmac.compare_digest(digest, request.headers.get('X-Shopify-Hmac-Sha256', ''))

FastAPI / Python

The usual Python mistakes, in order:

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

app = FastAPI()
SHOPIFY_SECRET = os.environ["SHOPIFY_CLIENT_SECRET"].encode()

@app.post("/webhooks/shopify")
async def shopify_webhook(request: Request):            # no dict / Pydantic body parameter
    body = await request.body()                          # raw bytes, before any parsing
    received = request.headers.get("x-shopify-hmac-sha256", "")
    digest = base64.b64encode(hmac.new(SHOPIFY_SECRET, body, hashlib.sha256).digest()).decode()
    if not hmac.compare_digest(digest, received):
        raise HTTPException(status_code=401, detail="HMAC verification failed")
    data = json.loads(body)                              # parse only after verifying
    return {"ok": True}

Shopify expects a 2xx within five seconds, so queue slow work instead of doing it in the handler. If deliveries aren't arriving at all, see Shopify webhook not firing.

The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. It verifies Shopify's X-Shopify-Hmac-Sha256 out of the box. Set SCHEME_<ID>=shopify. It works the same way for Stripe, Lemon Squeezy, Paddle Billing, Square, Twilio (status callbacks) and GitHub.

Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.

Free download: Webhook debugging cheat sheet.

Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint.