Shopify webhook HMAC verification failing
Shopify puts a base64-encoded HMAC-SHA256 of the raw request body in X-Shopify-Hmac-Sha256. Three details break the check: comparing against a hex digest, hashing a parsed body, and using the wrong secret.
Open the Shopify webhook tester →
The check
import crypto from 'node:crypto';
function verifyShopify(rawBody, hmacHeader, secret) {
const digest = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
const a = Buffer.from(digest);
const b = Buffer.from(hmacHeader || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}$raw = file_get_contents('php://input');
$calc = base64_encode(hash_hmac('sha256', $raw, $secret, true));
$ok = hash_equals($calc, $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] ?? '');import base64, hashlib, hmac
digest = base64.b64encode(hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()).decode()
ok = hmac.compare_digest(digest, request.headers.get('X-Shopify-Hmac-Sha256', ''))Cause 1: base64, not hex
Most HMAC examples end with .digest('hex'). Shopify's header is base64. In PHP, the fourth argument of hash_hmac must be true (raw binary) before base64_encode.
Cause 2: the body was parsed
Hash the bytes of the request, before any JSON parsing. In Express use express.raw({ type: 'application/json' }) on the webhook route; in Next.js await req.text().
Cause 3: the wrong secret
Webhooks created by an app are signed with the app's client secret, not with an access token. Webhooks created in the Shopify admin (Settings → Notifications → Webhooks) are signed with the key shown on that page.
Frequently asked questions
Can I test without creating a real order?
Yes: Webhook Toolkit's signer builds a payload with a valid X-Shopify-Hmac-Sha256 for your secret and sends it to your handler, and the admin's “Send test notification” button delivers a sample to any URL.