A customer edits an order on Tuesday morning. Shopify takes the change. Your ERP exported the first version at 06:00 and will not take it back. By Friday the warehouse has picked the wrong item, finance has posted the wrong total, and someone is retyping all three systems by hand.
An ERP is the system that holds stock, buying, shipping and finance records in one place. That is not an ERP fault or a Shopify fault. It is an ownership fault. Two systems were allowed to change one field, and nobody wrote down which one wins.
An ERP Shopify integration works when every shared field has one owning system, one write direction, and a rule for conflicting updates. Webhooks start the work; a scheduled sweep proves it finished. Test the awkward journeys, an edited order, a split shipment, a refund, before go-live, not after.
This guide is for the person who owns the orders after launch
This is for the Shopify or Shopify Plus lead who owns orders, stock and the handoffs between systems. You run an ERP now, or you are about to buy one. Your real test: does Monday morning need a person to fix things?
It is not for a team picking an ERP; that choice runs on other rules. It is also not for a store whose only add-on is a shipping app: if one system writes and the rest read, this is overhead.
Autonomous Technologies is a Shopify systems agency. We build and run the revenue, data and operations systems behind Shopify and Shopify Plus stores, for the operators accountable after launch. The map below is the one we draw before writing a line of code in Shopify integrations and custom apps. A public scan of a storefront cannot see a private ERP record or a sync log.
Ownership is a field-level call, not a system-level one
“The ERP is the source of truth” is too broad to settle a fight. The warehouse knows what shipped. Shopify owns the order the customer sent. Your accounts system owns the money posting. The design has to say how those three facts connect.
For each record, write down four things. The stable id. The field owner. The systems allowed to write. The sync direction. Where two systems can change one field, say how a clash is spotted and which side wins. Last to arrive is not the same as newest.
| Record | Ownership decision | Rule to write down |
|---|---|---|
| Product and variant | Who owns ids and copy? | Map the ids and name the fields Shopify may edit. |
| Stock | Who works out what is sellable at each place? | Fix the states, the holds and the write direction. |
| Order | Who may edit an order after export? | Fix the export trigger and the edits allowed after it. |
| Shipment | Which system confirms the shipment and tracking? | Tie each shipment to the right order and items. |
| Money | Who owns posting and the monthly tie-out? | Map the references and send clashes to finance. |
Write the map for whoever hunts a failure at 08:00. Every store we map gets the same first artefact: one page of records, owners and write directions.
Assign ownership before you connect anything. For each shared field, name the owning system, the systems allowed to write to it, and the rule that settles two updates arriving out of order. A field with no owner becomes a weekly manual repair.
Shopify’s five stock states are not interchangeable
Two systems can show different stock numbers and both be right, because they name different states. Shopify’s Help Center lists five, checked 21 September 2026: on hand, available, committed, unavailable and incoming.

| State | What Shopify means by it | Why it breaks a sync |
|---|---|---|
| On hand | Every unit you hold at a place | It matches the warehouse count, so it looks like the field to map. |
| Available | Stock you can sell | It is the only number that decides if checkout works. |
| Committed | Units in an unfilled order, a draft order or a transfer ready to ship | It moves on every order event, so a nightly whole-number push wipes it. |
| Unavailable | Units held by apps, or damaged, on hold, or safety stock | Most ERPs do not model it, so the gap reads as a bug. |
| Incoming | Stock on its way from a transfer or an app | Not sellable until it lands, so exporting it oversells. |
Map what the number means, not just the field name. Then pick one: does the sync write a whole number, or a change to it?
Shopify’s inventorySetQuantities call writes whole numbers and can check first. You send the number you think is stored. The write fails if it has moved. Shopify warns that skipping that check with ignoreCompareQuantity “can lead to inaccurate inventory quantities if multiple requests are made concurrently”.
Compare the same state, at the same place, at the same moment. A warehouse count and a Shopify sellable count can differ for a good reason. Agree the states and the hold rules before anyone calls the gap a bug.
Webhooks retry eight times over four hours, then stop
A webhook is a message Shopify sends your code when an event you signed up for happens, such as a new order. It starts work. It does not prove the work finished.
Shopify’s own docs say “webhook delivery isn’t always guaranteed”, and that Shopify “doesn’t guarantee ordering within a topic, or across different topics for the same resource”. So arrival order is not business order. A missing message is a normal Tuesday.
| What Shopify documents | The figure | What your build does about it |
|---|---|---|
| Connection timeout | 1 second | Confirm receipt before the work starts. |
| Whole-request timeout | 5 seconds | Queue the job, never run it inside the reply. |
| Retries after no reply or an error | 8 tries over 4 hours | Treat a 4 hour outage as total loss, then catch up. |
| Failures in a row before deletion | 8, for an Admin API subscription | Alert on the failure rate, not on the missing orders. |
| Repeat sends | Possible after a network timeout or a retry | Store `X-Shopify-Webhook-Id` and skip a repeat. |
Idempotent means handling the same message twice leaves the same result as handling it once. Shopify asks for that, and names X-Shopify-Webhook-Id as the key to store. That id stops repeat sends. It will not stop a second order made by your own retry path, so keep a stable business id in the other system too.
The same page asks for a sweep job: on a schedule, re-read Shopify for a set of records and a time window, then compare with the other system. Write the compare rule first, or the sweep just makes a longer report.
Exceptions need an owner, a queue and a checked repair
A log line tells a coder that something threw. It does not tell you that order 10482 needs a human before 14:00. Build an exception view with five columns: the record, the reason, first seen, last try, and the team on the hook. Keep customer detail out of it.

Split a short outage from bad data. A target that threw errors for ten minutes earns a retry. An unmapped product code, or SKU, does not. Retry it a hundred times and the queue fills while the broken mapping hides.
After a repair, check the business result, not the job status. “Retry succeeded” has to mean the right order exists once, with the right stock state and shipping record.
Pick a connector on coverage and running cost, not the feature list
Compare three routes against the same list of journeys.
- A supported connector. Right when its data model already covers what you need. Check the ERP version, the objects it handles, and who owns support.
- An integration platform. Right when you need reshaping, several systems or managed routing. Ask who keeps the flows running in month nine.
- Custom code. Right when neither of the others can meet a real need safely. Scope the upkeep and tests too.
Make every vendor show your awkward cases. An order edited after export. A split shipment. A missing product mapping. A target that is down.
Test the awkward journeys before go-live, then again every quarter
Run each case in a test store, from a known start state, against a result you agreed in writing. Log the input, expected result, real result and proof. An untested case is not a pass.
| Journey | Bar it has to clear |
|---|---|
| New eligible order | One record in the ERP, matching ids, agreed values. |
| Repeat send | No second order and no repeat stock effect. |
| Target system down | Work stays recoverable and the owner sees it. |
| Order edited after export | Each system takes the change or raises an exception. |
| Part shipment | The right items ship and the rest holds its agreed state. |
| Missed update | The sweep finds the gap and the repair keeps newer changes. |
Then diarise the re-run. Shopify ships a new API version, a dated release of the interface your connector talks to, every three months on the first day of the quarter. Each stable version lasts at least 12 months, with at least nine months of overlap. Miss that window and Shopify falls forward to the oldest version still open. Your sync then changes behaviour with nobody deploying a thing.
Keep the result, not the green tick. A passed test has an input, an expected result, the records you saw, and proof someone else could repeat. Re-run the affected tests whenever a connector, an API version or a business rule changes.
What breaks after go-live, and who owns it
The fair objection: you already have a connector from the app store, orders reach the ERP, so why draw a map at all? Because every failure below hits a working connector. Four of the five are not connector bugs.
| What breaks | What you see | Who owns it |
|---|---|---|
| The subscription is deleted after 8 failures in a row | Orders stop arriving, with no error and no deploy | Whoever runs the sync, alerting on the failure rate. |
| The API version falls forward | Field behaviour shifts on a quarter boundary | Whoever holds the app, re-running the test list each quarter. |
| Two systems write one field | The same order flips between two states | The field owner named in the map above. |
| Nobody reads the exception queue | A customer reports the same error a third time | You, reading the queue on a fixed day. |
| A product mapping is missing | One SKU fails, retries a hundred times, hides the rest | Whoever owns the catalogue, fixing the map before the retry. |
On Container One, an industrial B2B store, the stated constraint was that “the storefront was only the visible edge”. The durable work sat between the tools: make customer state explicit, check real production behaviour before trusting a flow, and leave the operating context with the team that ships releases.
Questions operators ask before sign-off
What is an ERP Shopify integration, in one sentence?
It is an agreed set of records moving between Shopify and your ERP. Each shared field has one owning system, one write direction, and a written rule for two updates that clash.
Do webhooks alone keep Shopify and my ERP in step?
No. Shopify says delivery is not guaranteed, order is not guaranteed, and a failing receiver gets 8 tries over 4 hours. A scheduled sweep catches the rest.
My warehouse count and my Shopify stock never match. Is the sync broken?
Often not. Shopify holds five stock states and only available is sellable. Compare the same state at the same place before you call it a bug.
How often should we re-test a live sync?
Every quarter at least, because Shopify ships a new API version on the first day of each quarter. Re-run affected tests at once when a connector or business rule changes.
What this gets back, in hours
Count the handoffs where a person retypes what another system already knows. That work does not shrink as you grow. It shrinks when each field has an owner and each exception has a queue.
| Manual step left in place | Minutes a day | Hours a year, over 240 days |
|---|---|---|
| Orders re-keyed into the ERP | 20 | 80 |
| Stock fixed by hand after a nightly push | 10 | 40 |
| Tracking numbers pasted back into Shopify | 5 | 20 |
If the job also moves old account and invoice data, keep that apart from daily order sync. Our B2B invoice migration guide covers the first. Merging both into one switch-over weekend is how dates slip.



