How to recover missed Shopify orders after a webhook failure
If your webhook endpoint was down for more than ~4 hours, Shopify's permanently dropped some orders. Here's how to find them and put them back into your system.
Symptoms
- Customer service is dealing with "where's my order" tickets that match a recent outage window.
- The Admin API shows orders your DB doesn't have.
- Your fulfillment / accounting / inventory system has a gap that lines up with a deploy or an incident.
- You discover the gap retrospectively during a quarterly audit and don't yet know how big it is.
Recovery — step by step
Step 1: Bound the outage window
You need two timestamps:
- When did webhooks stop reaching your system? Look at the timestamp of the last successfully-processed event. If you log
X-Shopify-Triggered-At, that's the firing time on Shopify's side. Otherwise use yourreceived_atminus a safety margin. - When did webhooks resume? The first event you processed after the gap.
Pad both ends by 30–60 minutes. Clock drift between Shopify and your servers, plus retries that arrive at the edge of the window, can leak events into either side.
Step 2: Pull every relevant order from the Admin API
For windows under a few thousand records, paginated GraphQL works:
query getOrders($cursor: String) {
orders(
first: 250,
after: $cursor,
query: "created_at:>='2026-05-09T02:00:00Z' created_at:<'2026-05-09T05:00:00Z'"
) {
edges {
node {
id
legacyResourceId
createdAt
updatedAt
}
}
pageInfo { hasNextPage endCursor }
}
}
For larger windows (hours-long outages on a high-volume shop), use a bulk operation:
mutation {
bulkOperationRunQuery(
query: """
{
orders(query: "created_at:>='2026-05-09T02:00:00Z' created_at:<'2026-05-12T00:00:00Z'") {
edges {
node {
id legacyResourceId createdAt updatedAt
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
Bulk operations don't count against the standard rate limits. The result is delivered as JSONL — one JSON object per line, streamable. Operations must complete within 10 days or they fail. On API version 2026-01 and higher, up to 5 bulk queries can run concurrently per shop. Poll currentBulkOperation (or subscribe to the bulk_operations/finish webhook) until status is COMPLETED, then download the result file from the returned URL.
Step 3: Diff against your local store
# Pseudocode
shopify_ids = bulk_result.map { |line| JSON.parse(line)["legacyResourceId"].to_s }.to_set
local_ids = Order.where("created_at >= ? AND created_at <= ?", from, to).pluck(:shopify_id).to_set
missing_ids = shopify_ids - local_ids
puts "Missing #{missing_ids.size} orders"
Most missing orders cluster in the middle of the outage. A handful may be on the edges — events that retried during the recovery and arrived just after you resumed. Always check before synthesizing to avoid duplicate processing.
Step 4: Fetch full payloads for each missing order
The bulk fetch returned only the fields you asked for. Your webhook handler likely needs the full order shape. Fetch each missing order via the standard GraphQL Admin API:
query {
order(id: "gid://shopify/Order/4321") {
id
legacyResourceId
createdAt
customer { id email firstName lastName }
lineItems(first: 100) {
edges {
node { id name quantity variant { sku } }
}
}
totalPriceSet { shopMoney { amount currencyCode } }
}
}
Be mindful of rate limits here — these are normal queries, not bulk. Standard plans get 100 points/second restored; Advanced 200; Plus 1000.
Step 5: Synthesize and replay through your handler
# Ruby example
require "openssl"
require "base64"
payload = {
id: order["legacyResourceId"].to_i,
order_number: order["name"].sub("#", "").to_i,
email: order["customer"]["email"],
total_price: order["totalPriceSet"]["shopMoney"]["amount"],
line_items: order["lineItems"]["edges"].map { |e| e["node"] }
}
raw_body = payload.to_json
hmac = Base64.strict_encode64(
OpenSSL::HMAC.digest("sha256", ENV["SHOPIFY_CLIENT_SECRET"], raw_body)
)
HTTP.post(
"https://your-app.com/shopify/webhooks",
body: raw_body,
headers: {
"Content-Type" => "application/json",
"X-Shopify-Topic" => "orders/create",
"X-Shopify-Hmac-Sha256" => hmac,
"X-Shopify-Shop-Domain" => "your-shop.myshopify.com",
"X-Shopify-Event-Id" => "reconciled-#{order['legacyResourceId']}",
"X-Shopify-Api-Version" => "2025-10",
"X-OurApp-Reconciled" => "1"
}
)
The custom header (above: X-OurApp-Reconciled) is your audit hook. When the handler logs received_at, it can also log received_via as "reconciliation" vs "realtime". Six months from now when a customer asks why their order shows a 6-hour delay between placement and processing, you need that distinction.
Edge cases that don't reconcile cleanly
- Inventory delta webhooks.
inventory_levels/updatereports a transition. The Admin API only shows current level — you can't reconstruct missed transitions. - App lifecycle webhooks.
app/uninstalledhas no recoverable Admin API equivalent. If you missed an uninstall, the next API call returns 401 — that's your signal to clean up. - Mandatory privacy webhooks.
customers/data_request,customers/redact,shop/redacthave a 30-day completion deadline and aren't reconcilable from the API. Track them through Shopify Partner records of affected shops. - Order edits and cancellations.
orders/edited,orders/cancelled,orders/fulfilleddescribe state transitions. If you missed a sequence, you may need to query both the order and its events to reconstruct the timeline.
Prevention
- Run hourly reconciliation against the Admin API for high-value topics like
orders/create. Any gap is caught within an hour, not days later. - Keep your webhook handler under the 5-second response window. Async-process everything except HMAC verification and persistence.
- Daily check on subscription health. Auto-re-register anything Shopify removed.
- Audit-log every reconciliation run. How many records checked, how many gaps found, how long it took. When a customer asks "why?", you need timestamps.
Where HookRescue fits
This entire reconciliation playbook runs automatically every hour for every active source you connect to HookRescue. When we find a gap, we synthesize a webhook-shaped payload, sign it with your client secret, and forward it to your endpoint exactly like a fresh delivery. Your handler doesn't need to know it was a recovery. Every reconciliation run is logged: how many records checked, how many gaps found, how long it took.
The setup is one URL change in your Shopify webhook subscription. We re-sign every event with your secret so HMAC verification continues to work unchanged. Free during private beta — no credit card.