Shopify webhook HMAC verification: fixing validation failures
Shopify signs every webhook using your app client secret with HMAC-SHA256, encoded as base64. Verification fails when comparing against hex or after body-parser runs.
Works with Shopify webhook events (orders, products, customers, inventory). Shopify signs every
outgoing webhook using your app’s client secret and sends the calculated digest in the X-Shopify-Hmac-Sha256 header.
The #1 cause: base64 vs hex encoding
Unlike Stripe and GitHub (which send hex-encoded signatures), Shopify encodes the HMAC-SHA256 hash as a base64 string.
If your code calls .digest('hex') instead of .digest('base64'), the comparison will always fail even when the raw
body and secret are 100% correct.
Verify your Shopify signature
Paste your raw request body and app client secret below to verify your digest against the X-Shopify-Hmac-Sha256 header:
Everything runs in your browser via the Web Crypto API. Your secret and payload are never sent anywhere.
Working code examples
import crypto from 'node:crypto';
export function verifyShopifyWebhook(rawBody: Buffer, headerHmac: string, secret: string): boolean {
const hash = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('base64');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(headerHmac));
}import hmac
import hashlib
import base64
def verify_shopify_webhook(raw_body: bytes, header_hmac: str, secret: str) -> bool:
digest = hmac.new(secret.encode('utf-8'), raw_body, hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest).decode('utf-8')
return hmac.compare_digest(computed_hmac, header_hmac)Other common failure reasons
- Parsed JSON body: Ensure middleware like
express.json()does not parse the stream before computing the hash. Always use raw bytes. - Wrong secret: For Custom Apps, use the API secret key. For Public Partner Apps, use the Client Secret from your Partner Dashboard.
- Header casing: HTTP/2 lowercases headers (
x-shopify-hmac-sha256). Access headers case-insensitively.
Keep reading
Start debugging your webhooks.
Point one endpoint at HookWatch, capture a failure, and replay it once it’s fixed. Free during beta.