Engineering · 7 min read ·
How to Verify Shopify Webhooks (HMAC Signature, Headers and Retries)
An unverified webhook endpoint is an open door. Here's exactly how Shopify signs webhooks and how to check the signature in Python and Node.

Shopify webhooks tell your app when something happens — an order is created, a product changes, an app is uninstalled. Your endpoint is a public URL, so anyone can send it a request that looks like a webhook. Verifying the signature is how you know it really came from Shopify.
How Shopify signs a webhook
Every webhook request includes a header, X-Shopify-Hmac-Sha256. Its value is the base64-encoded HMAC-SHA256 of the raw request body, computed with your webhook secret:
- For apps, the secret is the app's client secret (API secret key) from the Partner Dashboard or Dev Dashboard.
- For webhooks created in the Shopify admin (Settings → Notifications), use the signing key shown there.
Other Shopify webhook headers you should use
| Header | Use |
|---|---|
X-Shopify-Topic | Which event it is, e.g. orders/create |
X-Shopify-Shop-Domain | Which store sent it (multi-store apps) |
X-Shopify-Webhook-Id | Unique delivery ID — use it to ignore duplicates |
X-Shopify-Triggered-At | When the event happened — use it to handle out-of-order deliveries |
X-Shopify-API-Version | The API version of the payload |
The three rules of verification
- Use the raw body. Compute the HMAC on the exact bytes received, before any JSON parsing. Re-serialising parsed JSON changes whitespace and breaks the signature — the most common bug.
- Compare in constant time. Use
hmac.compare_digest(Python) orcrypto.timingSafeEqual(Node) so the comparison can't be timing-attacked. - Reject before doing anything. Return 401 and stop if the signature doesn't match.
Python example
import base64, hashlib, hmac
def verify_shopify_webhook(raw_body: bytes, hmac_header: str, secret: str) -> bool:
digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
expected = base64.b64encode(digest).decode()
return hmac.compare_digest(expected, hmac_header or "")
This is the logic packaged in our dependency-free shopify-webhook-validator, which you can drop into Flask, FastAPI or a serverless function.
Node.js example
import crypto from "node:crypto";
export function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(hmacHeader || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
In Express, read the body with express.raw({ type: "application/json" }) on the webhook route so you still have the raw bytes.
Respond fast, process later
Shopify expects a 2xx response within a few seconds. If your endpoint is slow or errors, Shopify retries the delivery several times over the following hours and can eventually remove a subscription that keeps failing. The robust pattern:
- Verify the HMAC.
- Check
X-Shopify-Webhook-Idagainst recently processed IDs; skip duplicates. - Put the payload on a queue and return 200 immediately.
- Process from the queue with retries and logging.
Don't rely on webhooks alone
Webhooks are "at least once" and occasionally late or missing. For critical data such as orders, run a periodic reconciliation job through the GraphQL Admin API to catch anything missed.
Building a custom Shopify app or integration? See our Shopify app development work and Shopify development service.
Frequently asked questions
Where do I find my Shopify webhook secret?
For apps, it is the app's client secret (API secret key). For webhooks created under Settings → Notifications in the admin, Shopify shows a signing key on that page.
Why does my Shopify HMAC verification always fail?
Usually because the HMAC is computed on parsed and re-serialised JSON instead of the raw request body, or with the wrong secret.
Can Shopify send the same webhook twice?
Yes. Deliveries are at least once. Use the X-Shopify-Webhook-Id header to detect and skip duplicates.


