Skip to content
SyncMyShop

Notes

Webhooks are not a guarantee: duplicates, wrong order and missed events

A webhook is delivered at least once, in no fixed order, and sometimes not at all. An integration that writes to the ERP straight from each webhook ends up with duplicate orders, old data over new data and orders that never arrive. Three parts prevent that: a duplicate check, a timestamp check and a reconciliation run that asks the API what the webhooks missed.

Updated

What a webhook promises

A webhook is an HTTP request that a platform sends to a fixed address when something changes: an order is created, a stock level moves. The platform promises to try. It does not promise that the event arrives once, that events arrive in the order they happened, or that every event arrives.

Shopify says so in its webhook documentation: delivery is not always guaranteed, ordering is not guaranteed within a topic or across topics for the same resource, and the same webhook can be delivered more than once. BigCommerce writes that its service does its best to deliver and that duplicate callbacks occasionally occur.

Property What the platform does What goes wrong in the ERP
Delivery At least once: repeated until a success response arrives Two sales orders for one store order
Order None. A products/update can arrive before the products/create An older version overwrites a newer one
Completeness Retries for hours, then gives up An order that never reaches the warehouse
Subscription Removed or deactivated after repeated failures Orders stop arriving and no call fails

Each row has its own check. A duplicate check does not put events in order, and neither check finds an event that never came.

The same event arrives twice

Shopify waits 5 seconds for the whole request, with 1 second to open the connection. Any response outside the 200 range counts as a failure, 3xx included. A receiver that creates the sales order in the ERP first and answers afterwards is over 5 seconds as soon as the ERP is slow. Shopify then sends the event again while the first one is still being processed.

Two rules stop the second sales order.

Answer first, process later. The receiver checks the signature, stores the delivery in a queue and returns 200 OK. The ERP call happens after that, at the pace the ERP allows.

Store the delivery ID. Every delivery carries an ID. The integration keeps the IDs it has processed and skips one it has seen:

X-Shopify-Topic: orders/create
X-Shopify-Webhook-Id: 7f1c2c0e-52a4-4f0b-9c3e-0d6f9a1b8e21   <- one per delivery
X-Shopify-Event-Id: 3b9e6a54-1d0c-4a77-b1b2-5e8f4c2d9a10     <- one per event
X-Shopify-Triggered-At: 2026-10-02T14:02:11.482Z

X-Shopify-Webhook-Id identifies the delivery. X-Shopify-Event-Id is shared by all deliveries that come from the same action, for example when two subscriptions listen to the same topic. BigCommerce puts a hash in the payload for the same purpose, and WooCommerce sends X-WC-Webhook-Delivery-ID.

The ID list covers the receiving side. The write to the ERP needs its own key: the store order number as the external ID of the sales order, so a second write lands on the record that already exists. That mechanism and the other sources of double records are in duplicate customers and duplicate orders.

Events arrive in the wrong order

Northfield Supply Co. places order #48212 at 14:02:11. Forty seconds later the buyer corrects the ship-to and adds "Suite 200". Shopify sends orders/create and then orders/updated. The first delivery of orders/create is lost to a timeout, so the update arrives first and the retry of the create arrives after it.

An orders/create webhook that arrives after orders/updated Order 48212 is placed at 14:02:11 and the buyer corrects the ship-to at 14:02:51. The first delivery of orders/create is lost to a timeout. The orders/updated webhook arrives first and creates the sales order in NetSuite with the corrected address. When Shopify retries orders/create, the integration sees that its updated_at of 14:02:11 is older than the stored 14:02:51 and skips it. Shopify store Integration queue and log NetSuite ERP orders/create #48212 updated_at 14:02:11 No 200 OK within 5 seconds. Shopify counts the delivery as failed and retries it later. orders/updated #48212 updated_at 14:02:51 200 OK upsert order SHOP-48212 ship-to: Suite 200 sales order created Shopify retries the failed delivery orders/create #48212 updated_at 14:02:11 200 OK 14:02:11 is older than 14:02:51: skip NetSuite keeps the corrected ship-to. Processed in order of arrival, the retry would have written the old address back.
The retry of orders/create arrives after orders/updated. The timestamp check keeps the corrected ship-to in NetSuite.

An integration that processes deliveries in order of arrival writes the address without "Suite 200" back over the corrected one. The delivery ID does not help here: the retried create was never processed, so it is not a duplicate.

The check that helps compares time. Shopify recommends the X-Shopify-Triggered-At header or the updated_at field in the payload. The integration stores the updated_at of the last version it wrote per order, 14:02:51 in the example, and skips any delivery that is older. It also has to accept an update for an order it has not seen: the orders/updated payload holds the full order, so the integration creates the sales order from it.

An event never arrives

Every platform stops retrying at some point. These rules were read in the vendors' documentation on October 3, 2026.

Platform Payload Retries After the last retry
Shopify Full order or product as JSON 8 retries over 4 hours Subscription deleted after 8 consecutive failures, when it was created through the Admin API
BigCommerce Light: for an order status change only the order id 11 retries, 60 seconds to 24 hours apart Webhook deactivated, email to the address of the app
WooCommerce Full order, customer or product as JSON Sent in the background through wp-cron Disabled after 5 consecutive failed deliveries
Business Central Reference only: resource and changeType Retries over 36 hours on a 408, 429 or 5xx Subscription deleted on any other response; expires after 3 days unless renewed

The BigCommerce schedule added up: 60 + 180 + 300 + 600 + 900 + 1,800 + 3,600 + 7,200 + 21,600 + 50,400 + 86,400 seconds is 173,040 seconds, 48 hours and 4 minutes. BigCommerce also tracks the success rate per receiving domain over a two-minute window. Below 90% the domain is blocklisted for three minutes, and a subscription without activity for 90 days is deactivated.

The last column is the one that costs orders. Take a store with 72 orders an hour and a receiver that is down from 13:00 to 18:00. Shopify retries the 13:00 event for 4 hours. At about 17:00 its eighth retry fails and the subscription is deleted. The receiver is back at 18:00 and gets nothing: 5 hours times 72 is 360 orders that sit in the store and not in the ERP, and no new webhook follows until the subscription is created again. Shopify sends a warning to the emergency developer email address of the app. Nothing fails on the receiving side, so nothing is logged there.

The second column decides how much work a webhook saves. BigCommerce and Business Central send a reference, and the integration fetches the record through the API. Business Central waits 30 seconds after the first change before it sends the notification, and sends one collection notification when more than 1,000 records change in that time. Those API calls count against the rate limit, as worked out in integration throughput.

The reconciliation run

The answer to missed events is a scheduled query that asks the API for everything that changed since the last successful run, and compares the result with the log of processed records. Shopify calls it a reconciliation job and recommends it for every app that uses webhooks.

query {
  orders(first: 100, query: "updated_at:>='2026-10-02T12:50:00Z'") {
    edges { node { id name updatedAt } }
    pageInfo { hasNextPage endCursor }
  }
}

Three details make it work.

The window starts at the last successful run. A run that always looks 25 minutes back finds 25 minutes of orders after a five-hour outage. In the example the last successful run was at 13:00. The first run after recovery asks for everything since 12:50, the last run minus 10 minutes of overlap. That is 310 minutes at 1.2 orders a minute: 372 orders in 4 pages of 100. Twelve of them, from 12:50 to 13:00, are in the log and are skipped. The other 360 are created.

The run checks the subscription. Before it queries orders, it asks the platform whether the webhook subscription exists and is active, and creates it again when it is gone.

The run uses the same checks as the webhook. An order found by the run and delivered by a late webhook a minute later is one order, because both paths go through the ID list and the external ID.

On a normal day the cost is small. A run every 15 minutes is 96 queries a day. At 72 orders an hour each run returns about 30 orders, all of them already processed. How the cadence per data stream is chosen is in real time versus batch.

What this means for your integration

Subscribing to a webhook is one API call. The work is behind it: a receiver that verifies the signature and answers within the limit, a queue, the duplicate check, the timestamp check and the reconciliation run. We build those as part of every integration, then host and run it. In the dashboard you see each delivery, whether it was processed or skipped and why, and what the reconciliation run found.

A deleted subscription or a growing queue raises an alert on our side, and what happens after that is described in what happens when an integration fails. The flows this applies to are on integrations, and the order flow with its fields on orders, customers and shipping. Tell us which store platform and ERP you run, and you hear which events they send and where a scheduled run is needed.

Frequently asked questions

Are webhooks reliable enough for an order sync?

As the fast path, yes. As the only path, no. Shopify states in its own documentation that webhook delivery is not always guaranteed and recommends a reconciliation job next to it. An order sync that uses webhooks for speed and a scheduled API query for completeness gets every order, and gets most of them within seconds.

What does at-least-once delivery mean?

The sender repeats a delivery until it gets a success response, so every event arrives one or more times. It does not promise exactly one delivery. When the response of the receiver is slow or lost, the sender delivers the same event again, and the receiver has to recognize it as one it already handled.

How does an integration recognize a duplicate webhook?

By an ID that the platform sends with every delivery. Shopify sends it in the X-Shopify-Webhook-Id header, BigCommerce as a hash field in the payload, WooCommerce in X-WC-Webhook-Delivery-ID. The integration stores each ID it has processed and skips a delivery whose ID is already in the list. On the ERP side the store order number as an external ID stops a second sales order.

What happens to webhooks while the receiving side is down?

The platform retries for a limited time and then stops. Shopify retries 8 times over 4 hours and deletes the subscription after 8 consecutive failures. BigCommerce retries 11 times over about 48 hours and then deactivates the webhook. Events whose retries ran out are not sent again; the integration has to fetch those records through the API.

How often should a reconciliation run be scheduled?

Often enough that a missed order is found before someone notices it is missing. Every 15 minutes is 96 API queries a day and puts the longest delay for a missed order at 15 minutes. The window of each run starts at the last successful run, not at a fixed number of minutes back, so an outage of five hours is covered by the first run after it.

If the reconciliation run finds everything, why use webhooks at all?

For speed and for the call budget. A webhook brings an order to the ERP within seconds, a 15-minute schedule brings it after 7.5 minutes on average. The webhook also carries the changed record, so the integration does not have to ask the API for changes that did not happen.

Tell us what you need

Tell us which systems you use and what needs to work better. We reply personally within one working day. We help you define the work, then agree the price and timing before you commit.

  • You know the price and the timeline before you commit to anything
  • We work out the connection that takes the work off your hands
  • We handle the build, hosting, monitoring and maintenance
  • Your team has less information to retype and fewer discrepancies to chase

Suppliers, products, orders per day, a rough estimate is fine.

Or email directly: carsten@syncmyshop.com