Shopify webhook not firing: debug checklist and recovery steps
If a Shopify webhook isn't reaching your endpoint, you have under 4 hours to fix it before Shopify drops the event permanently. Here's the diagnosis path most teams miss.
Symptoms — what you are probably seeing
- Customers report orders or refunds your system never recorded.
- Your webhook handler logs show silence for a topic that used to be busy.
- The Admin API shows recent records that aren't in your DB.
- Sentry / your error tracker shows a spike of 5xx or timeout errors followed by silence.
- The "your webhook is failing" warning email is sitting in a Partner-account admin inbox no engineer reads.
The usual causes, ranked
From production incidents in this category, the failures cluster into six buckets:
- 5-second timeout exceeded. Your handler is doing something synchronously that occasionally crosses the threshold — a third-party API call, a slow DB write under contention, an image resize. Shopify counts every timeout as a failure even if the work eventually completed. This is the single most common cause.
- Subscription was auto-removed. 8 failed attempts within 4 hours triggers automatic removal. New events stop arriving entirely because they were never sent. Full recovery flow →
- HMAC verification failing silently. Your framework JSON-parsed the body before you read raw bytes. The signature you compute over the re-serialized body doesn't match what Shopify sent. Handler returns 401, every retry hits the same 401, Shopify gives up.
- API version mismatch. Shopify versions webhook payloads. If your subscribed version is no longer supported, Shopify falls forward to the oldest supported stable version. The payload shape changes underneath you and your parser breaks. The
X-Shopify-Api-Versionresponse header is your signal — if it differs from your subscribed version, you're on a fall-forward. - TLS or DNS failure. Cert expiry, hostname mismatch on a wildcard cert, DNS misconfig. Shopify never even completes the TLS handshake. Counts as a failure same as a 5xx.
- App access scopes were reduced. If a merchant or your app config dropped scopes, Shopify only sends events the app is still authorized to receive. Topics you used to get can simply stop firing.
Recovery — diagnosis in 5 steps
Step 1: Confirm the subscription is registered
query {
webhookSubscriptions(first: 50) {
edges {
node {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
format
apiVersion {
handle
}
}
}
}
}
If the topic you expected is missing, jump straight to the subscription recovery flow. No amount of endpoint debugging helps if the events are not being sent.
Step 2: Prove your endpoint responds 200 in under 5 seconds
time curl -X POST https://your-app.com/shopify/webhooks \
-H "Content-Type: application/json" \
-H "X-Shopify-Topic: orders/create" \
-H "X-Shopify-Hmac-Sha256: dummy" \
-H "X-Shopify-Shop-Domain: example.myshopify.com" \
-H "X-Shopify-Event-Id: test-event-1" \
-d '{"id":1,"order_number":1}'
The full request should complete in well under 5 seconds. If yours is anywhere near 4 seconds even occasionally, you are losing webhooks. The fix is the same in every framework: read the raw body, persist it to a queue, return 200, then process async.
Step 3: Re-verify HMAC against the raw body
Compute the HMAC yourself outside the request lifecycle to rule out verification bugs:
# Ruby example
require "openssl"
require "base64"
raw_body = '{"id":1,"order_number":1}' # exact bytes Shopify sent
secret = ENV["SHOPIFY_CLIENT_SECRET"]
expected = Base64.strict_encode64(
OpenSSL::HMAC.digest("sha256", secret, raw_body)
)
# Compare with what Shopify sent in X-Shopify-Hmac-Sha256
# Use ActiveSupport::SecurityUtils.secure_compare for constant-time check
The most common bug here is reading the body after the framework already parsed it. In Rails, capture in middleware:
class RawBodyCaptureMiddleware
def initialize(app); @app = app; end
def call(env)
if env["PATH_INFO"].start_with?("/shopify/webhooks") && env["REQUEST_METHOD"] == "POST"
env["shopify.raw_body"] = env["rack.input"].read
env["rack.input"].rewind
end
@app.call(env)
end
end
Step 4: Check the Shopify Dev Dashboard webhook delivery report
If your app was created in the Dev Dashboard or via Shopify CLI, you have a 7-day webhook delivery report there. Filter by topic and shop. If you see a string of 5xx or timeout responses leading up to the silence, the cause is on your side. If Shopify never even attempted delivery, the subscription is gone or the event never fired (less common — Shopify-side incident).
Step 5: Reconcile from the Admin API
Anything Shopify already gave up on is gone from their queue. Pull the records from the Admin API and replay through your handler:
mutation {
bulkOperationRunQuery(
query: """
{
orders(query: "created_at:>='2026-05-09T02:00:00Z' created_at:<'2026-05-09T05:00:00Z'") {
edges {
node {
id legacyResourceId createdAt
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
Bulk operations don't count against the standard rate limits and can complete on their own schedule (up to 10 days). Poll currentBulkOperation until COMPLETED, download the JSONL result, diff against what you have locally, and replay the missing IDs.
For full reconciliation flow including HMAC re-signing of synthesized events, see recover missed Shopify orders.
Prevention — stop this happening again
- Respond 200 immediately, process async. The single biggest reliability fix. Use Sidekiq, BullMQ, SQS — anything that decouples receipt from work.
- Read the raw body in middleware before any framework JSON-parses it. HMAC computes over those bytes, not your re-serialized version.
- Use
X-Shopify-Event-Idfor deduplication. Same value across retries.X-Shopify-Webhook-Idis the subscription ID, not the event ID. - Daily cron checking
webhookSubscriptions. Alert your engineers (not just your Partner email) on any missing topic. - Hourly reconciliation against the Admin API for high-value topics like
orders/create. Catches Shopify-side delivery issues and gaps from removed subscriptions. - Watch
X-Shopify-Api-Versionin incoming webhooks. If it doesn't match your subscribed version, your version got deprecated and Shopify is falling forward — your parser may break. - Forward the Partner emergency developer email to a place your team actually reads.
Where HookRescue fits
HookRescue sits between Shopify and your existing handler. Every event gets re-signed with your secret and forwarded to your endpoint, so your HMAC verification keeps working unchanged. If your endpoint is down, we retry across 7 days instead of Shopify's 4 hours. If a subscription gets auto-removed, we re-register it. Every hour, we reconcile against the Admin API and synthesize any gap. You skip the diagnosis sequence above because we never give up on the event in the first place.