How to recover missed Shopify webhooks
If your endpoint was down for an hour, Shopify may have given up. Here's how to detect missed webhooks via the Admin API and replay them with full HMAC integrity.
A customer asks why their order didn't show up in your fulfillment system. You check the logs — no record of the webhook ever arriving. Shopify's records show the order exists. Where did the webhook go?
Why webhooks go missing
There are four common causes for "Shopify processed an event but my system never saw it":
- Endpoint was down during the entire 4-hour retry window. Rare, but a hardware failure or extended outage can do it.
- HMAC verification quietly rejected it. Your secret rotated but Shopify still has the old one — every webhook returns 401, Shopify retries 8 times, then gives up.
- Webhook subscription was auto-unsubscribed. Once 8 retry attempts fail within Shopify's 4-hour retry window (per the policy updated September 10, 2024), the subscription is removed. Shopify sends a warning email to your Partner emergency developer email when this happens — but if that address isn't actively monitored, the subscription stays gone until you re-register via the Admin API.
- Shopify-side delivery bug. Status-page incidents do happen — events are delayed or dropped on their end.
The first three are the operationally common ones. All three look identical from your side: nothing arrived.
How to detect missed webhooks
The only reliable detection is to compare what you have against what should exist in the source of truth. For Shopify, that's the Admin API. The skeleton:
# Fetch orders created in the last hour from Shopify
since = 1.hour.ago.iso8601
shop_orders = ShopifyAPI.bulk_fetch("orders", filter: "created_at >= '#{since}'")
# Compare against what we received
local_ids = Order.where("created_at >= ?", since).pluck(:shopify_id).to_set
missing = shop_orders.reject { |o| local_ids.include?(o.id) }
missing.each do |order|
Webhook.synthesize(topic: "orders/create", payload: order)
end
The hard parts: rate limits (Shopify's GraphQL Admin API is leaky-bucket; you'll hit limits), batching (you don't want to fetch 50,000 orders/hour for every store), per-topic logic (orders are easy to reconcile; products and inventory are harder), and idempotency (synthesizing an event you actually received before would create a duplicate).
Recovering events you've already missed
If you suspect events were missed in a specific window — say, an outage from 02:00 to 05:00 yesterday — the recovery flow is:
- Pull every relevant Admin API record created in that window.
- Cross-reference against your local store. Build a list of missed IDs.
- For each missed ID, fetch the full record and feed it through your normal webhook handler — the same code path, just synthesized payloads.
- Tag the synthesized records so you know they came from reconciliation, not delivery. Helpful when debugging later why a record has a different
received_atthan itscreated_at.
This works for orders, products, customers — anything with a stable created_at and a list endpoint. It doesn't work as cleanly for inventory deltas (no list endpoint that shows a "before/after") or for app/* topics (uninstall events have no Admin API equivalent).
Preventing the next miss
The preventive measures matter more than recovery:
- Monitor your subscription health. Periodically GET the webhook subscription via Admin API and check it's still active.
- Use a longer retry curve than Shopify's 4 hours. Anywhere from 5 to 14 days for receipt-side retries, so a long deploy or DB migration doesn't drop events.
- Reconcile on a schedule. Hourly for hot topics (orders), daily for slower (products). You should never go a day without comparing against the API.
- Alert on attempt-count anomalies. If your forward attempts suddenly spike, something is failing — investigate before retries exhaust.
- Audit-log every replay. When you do recover events, you want to know exactly which were synthesized and when.
How HookRescue recovers missed webhooks
HookRescue reconciles every active source against Shopify's Admin API on a configurable cadence (hourly for paid plans, daily for free). When it finds a gap, it synthesizes the event with full HMAC integrity using your signing secret and forwards it to your endpoint exactly like a fresh delivery. Your handler doesn't need to know it was a recovery.
You also get a dashboard view of every reconciliation run: how many records checked, how many gaps found, how long each run took. So when something does slip through, you have an audit trail.