How to Debug Shopify Webhooks in 2026
Debug Shopify webhooks fast: verify the X-Shopify-Hmac-Sha256 header, fix 401s, survive the 19-retry deletion rule, and replay real payloads to localhost.
Ozer
Developer & Founder of HookSense
Debugging Shopify webhooks comes down to three checks: confirm the delivery is actually arriving (capture it with an inspector so you can see the raw request), verify the X-Shopify-Hmac-Sha256 header against the raw request body with the correct secret, and respond with a 2xx within 5 seconds. Most "my webhook is broken" reports trace back to one of those three — a parsed body breaking HMAC verification, the admin-vs-app secret mix-up, or a slow handler that quietly burned through Shopify's 19 retries until the subscription was deleted. This guide walks through each failure mode and a workflow for fixing them quickly.
The Shopify Webhook Lifecycle
When something happens in a shop — an order is created, a product is updated, an app is uninstalled — Shopify looks up the webhook subscriptions registered for that topic and sends an HTTP POST to each one. The lifecycle has a few properties that bite developers who are used to Stripe or GitHub webhooks:
- An event occurs in the shop, scoped to a topic like
orders/createorproducts/update. - Shopify POSTs the payload as JSON to your subscription's address, signed with an HMAC.
- Your server has 5 seconds to return a 2xx status. Not 20, not 30 — five. Anything slower counts as a failed delivery, even if your handler eventually succeeds.
- Failed deliveries are retried 19 times over roughly 48 hours, with increasing backoff between attempts.
- If all 19 retries fail, Shopify deletes the subscription. Your app stops receiving that topic entirely, with no error in your logs, until you re-register the webhook.
That last step is the most famous Shopify webhook gotcha. A two-day outage does not just mean a backlog — it means the pipe itself is gone. Production apps should periodically reconcile their registered webhooks against the list they expect, and re-create anything missing.
Know Your Headers
Every Shopify webhook delivery carries metadata in headers, and reading them is the fastest way to orient yourself when debugging:
X-Shopify-Topic— the event topic, e.g.orders/create. If your handler routes on the URL path instead of this header, a copy-pasted registration can silently send the wrong topic to the wrong handler.X-Shopify-Shop-Domain— themyshopify.comdomain of the shop that generated the event. Essential for multi-tenant apps, and the first thing to check when "missing" webhooks turn out to be arriving from your dev store instead of the production shop.X-Shopify-Webhook-Id— a unique ID per delivery. Use it as your idempotency key, because retries deliver the same event again and your handler will run more than once.X-Shopify-Hmac-Sha256— the base64-encoded signature you must verify before trusting anything in the body.X-Shopify-API-Version— the API version the payload was serialized with. Payload shapes change between versions, so log this and pin your subscriptions to a specific version.
Verifying the HMAC Correctly
Shopify signs every webhook by computing an HMAC-SHA256 over the raw request body bytes, base64-encoding the digest, and sending it in X-Shopify-Hmac-Sha256. Note the encoding: Shopify uses base64, while Stripe and GitHub use hex — a detail that trips up developers porting verification code between providers. Your job is to recompute that digest with the same secret and compare it timing-safe:
const crypto = require('crypto');
function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
const digest = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('base64');
const a = Buffer.from(digest, 'base64');
const b = Buffer.from(hmacHeader || '', 'base64');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The two rules that matter:
- Raw bytes only. The HMAC is computed over the exact bytes Shopify sent. If a JSON body parser runs before your verification —
express.json(), a framework's automatic parsing, a proxy that re-serializes — key order, whitespace, and unicode escaping can change, and verification fails even though the payload is genuine. Capture the raw body on the webhook route:
app.post('/webhooks/shopify',
express.raw({ type: 'application/json' }),
(req, res) => {
const hmac = req.get('X-Shopify-Hmac-Sha256');
if (!verifyShopifyWebhook(req.body, hmac, secret)) {
return res.status(401).send('HMAC mismatch');
}
res.status(200).send('ok'); // ack fast, process async
processWebhook(JSON.parse(req.body));
});
- Timing-safe comparison. A plain string equality check leaks timing information that lets an attacker forge signatures byte by byte. Use
crypto.timingSafeEqual(and check lengths first, since it throws on unequal buffers). For the full reasoning, see our guide to HMAC best practices for webhook security or the HMAC glossary entry.
The Two-Secrets Gotcha
Shopify has two ways to register a webhook, and they are signed with different secrets:
- Admin-created webhooks — added manually in the Shopify admin under Settings → Notifications → Webhooks. These are signed with the dedicated signing secret displayed at the bottom of that page.
- API-created webhooks — registered programmatically via the GraphQL Admin API's
webhookSubscriptionCreatemutation (or the REST equivalent). These are signed with your app's API secret (the client secret from your app configuration).
If you build your handler against an admin-created webhook during prototyping, then switch to API registration when your app goes live — or vice versa — every delivery starts failing with a 401 and nothing in the payload tells you why. When verification fails, the very first question to ask is: which path created this subscription, and which secret am I using? Paste the captured raw body and your candidate secret into an HMAC calculator and compare against the header — if one secret produces a match and the other does not, you have your answer in thirty seconds.
The 5-Second Rule and Death by 19 Retries
Shopify gives your endpoint 5 seconds to respond. That budget includes TLS handshake, any middleware, and your handler. Synchronously writing to a database, calling a third-party API, or sending an email inside the handler is the classic way to blow it.
The fix is the same as for every webhook provider, just with less slack: acknowledge first, process later. Verify the HMAC, persist or enqueue the payload, return 200, and do the real work in a background job keyed on X-Shopify-Webhook-Id for idempotency.
What makes Shopify unforgiving is what happens when you consistently miss the window. Each failed delivery is retried 19 times over about 48 hours. After the 19th failure, Shopify deletes the webhook subscription. The shop keeps generating events; you just stop hearing about them. Defenses worth building:
- Reconcile subscriptions on a schedule — query the webhooks that exist for each shop and re-register any that are missing.
- Re-register on app load or OAuth refresh — many production Shopify apps idempotently re-create their subscriptions every time a merchant opens the app.
- Treat sustained 4xx/5xx responses as an incident, not a nuisance — the 48-hour clock is already running.
Pin Your API Version
Shopify versions its Admin API quarterly, and webhook payloads are serialized according to the version the subscription was created with. Fields appear, change shape, or disappear between versions; some topics only exist from a given version onward. If your code assumes a field that your subscription's version does not emit, you get undefined-property crashes that look random.
Always set an explicit API version when registering subscriptions, log the X-Shopify-API-Version header on every delivery, and schedule version upgrades deliberately — diffing a captured payload from the old version against one from the new version before flipping the switch.
Mandatory Privacy Webhooks
If you distribute a public app on the Shopify App Store, three GDPR/privacy webhooks are mandatory, and Shopify validates that your endpoints exist and verify HMACs correctly during app review:
customers/data_request— a customer asked the merchant for their data; your app must provide what it stores.customers/redact— delete the personal data you hold for a specific customer.shop/redact— sent about 48 hours after a merchant uninstalls; delete the shop's data.
A common review rejection is returning 200 to these endpoints without verifying the HMAC — Shopify probes them with invalid signatures and expects a 401. Test that path explicitly.
A Field Guide to Common Failures
- 401 on every delivery: verifying the parsed body instead of raw bytes, or using the app API secret against an admin-created webhook (or the reverse).
- Verification fails only in production: a proxy, CDN, or framework middleware mutates the body before your handler sees it.
- Webhooks silently stopped: subscription deleted after 19 failed retries — re-register and add reconciliation.
- Events "missing": you are watching the production shop while the subscription lives on your dev store, or vice versa. Check
X-Shopify-Shop-Domainon what does arrive. - Handler crashes on a missing field: API version mismatch between what you coded against and what the subscription emits.
- Duplicate side effects: retries delivered the same event twice and your handler is not idempotent on
X-Shopify-Webhook-Id.
A Faster Debugging Workflow with an Inspector
Every failure above gets dramatically easier to diagnose when you can see the exact request Shopify sent — raw body, every header, byte for byte. That is what a webhook inspector gives you:
- Get a capture URL. Open the Shopify webhook tester and you have a free, unique URL in about a second — no signup required.
- Point Shopify at it. Register the URL as a subscription (admin or API) and trigger a real event: create a test order, update a product.
- Inspect the real payload. The delivery appears in real time with the full raw body and all the
X-Shopify-*headers. No more guessing what the payload for your API version actually looks like. - Verify the HMAC against the capture. HookSense supports Shopify signature verification natively — paste in your secret and it checks the captured delivery, so you can test both candidate secrets and settle the admin-vs-app question immediately. The standalone HMAC calculator works for one-off checks too.
- Replay to localhost. Run
npx hooksense listen -p 3000and captured webhooks are forwarded to your local server (see the CLI docs). Replay any past delivery as many times as you need — with edits, if you want to simulate a malformed payload or a different topic — instead of creating a fresh test order for every iteration.
Because captured payloads stick around (14 days on the free Catch plan, 30 on Hook, 90 on Sense, encrypted at rest with AES-256-GCM), you can debug Monday's failure on Thursday, or share the exact failing request with a teammate instead of describing it.
Wrapping Up
Shopify webhooks are reliable once your handler respects their rules: verify the base64 HMAC over the raw body with the right secret, answer inside 5 seconds, stay idempotent across retries, pin your API version, and never let 19 failures pile up unnoticed. When something does break, capture the real delivery first and reason from bytes, not assumptions. For the full setup walkthrough — registration via GraphQL, topic reference, and handler templates — see the Shopify webhook integration guide.
Related posts
Try HookSense Free
Inspect, debug, and replay webhooks in real-time. No credit card required.
Get Started Free