Webhook Debugging Guide

Validating Shopify webhook HMAC signatures

Short answer

Shopify sends X-Shopify-Hmac-Sha256: a base64-encoded HMAC-SHA256 of the raw request body, keyed with your app's client secret — not the API key, and not a per-webhook secret like Stripe uses. Compute the same HMAC over the exact bytes you received, in a constant-time comparison, and compare.

Node.js

const crypto = require('crypto');

function verifyShopifyWebhook(rawBody, hmacHeader, clientSecret) {
  const digest = crypto
    .createHmac('sha256', clientSecret)
    .update(rawBody, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(digest),
    Buffer.from(hmacHeader)
  );
}

rawBody must be the unparsed body: mount express.raw({ type: 'application/json' }) on the webhook route before any JSON body parser touches it, the same issue as Stripe.

Python

import hashlib, hmac, base64

def verify_shopify_webhook(raw_body: bytes, hmac_header: str, secret: str) -> bool:
    digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    return hmac.compare_digest(base64.b64encode(digest).decode(), hmac_header)

Why it fails

A wrong secret is the usual cause — the client secret lives in your Partner Dashboard app settings, separate from the API key used for Admin API calls. The other common cause is a re-parsed body: any middleware that parses and re-serializes JSON before you verify changes the bytes and breaks the HMAC. Always compare digests with timingSafeEqual or compare_digest, never === or ==, so a failed check can't be timed to guess the secret byte by byte.

Shopify has no timestamp in the signed payload, so there's no tolerance window and no concept of a signature going stale — a captured payload verifies the same today as it did the day it arrived.

The easier way: WebhookMon

WebhookMon verifies Shopify's HMAC on arrival and shows the verdict next to every event, with the exact header and body it hashed if you need to check your own implementation against it. Forward events to your local server and replay any of them, byte for byte, whenever you need to.

WebhookMon's Signature tab for a Shopify orders/create event: Signature does not match: the secret is wrong, with the X-Shopify-Hmac-Sha256 value it checked and the body size
Download WebhookMon free trial Learn more

Related guides