Connecting Shopify to an ERP, WMS or 3PL: How to Design It
What flows each way between Shopify and an ERP, warehouse system or 3PL, who owns each record, and how to handle retries, bulk loads, failures and reconciliation.
In this guide
- What usually flows, and in which direction
- Decide the source of truth per field, not per system
- Two ways to connect a warehouse
- Webhooks, polling and bulk operations
- Idempotency: writing back without doubling
- Failure handling
- Reconciliation: proving the two sides agree
- Off-the-shelf connector or custom integration
- Testing before it touches real orders
- FAQ
- Getting it built
Once a store outgrows packing orders by hand, Shopify stops being the only system that matters. An ERP holds costs, purchase orders and the books. A warehouse system (WMS) or third-party logistics provider (3PL) holds the stock and does the picking. Each needs some of Shopify’s data, and Shopify needs some of theirs back.
This guide walks through how to design that connection: what usually flows in each direction, which system should own each record, how to move data without losing or doubling it, and how to tell when it has drifted. It assumes you already know the general failure patterns of integrations; if not, read Shopify integrations that don’t break first, because this post builds on it rather than repeating it.
What usually flows, and in which direction
Most ERP, WMS and 3PL connections move the same six kinds of record. The direction is what differs between businesses, so write it down before anyone writes code.
| Record | Usual owner | Usual direction | Typical trigger |
|---|---|---|---|
| Orders | Shopify | Shopify → ERP / WMS / 3PL | New or updated order |
| Stock levels | WMS or 3PL | Warehouse → Shopify | Receipt, count, pick, adjustment |
| Products and variants | ERP or PIM, sometimes Shopify | One way only, decided up front | New SKU, cost or attribute change |
| Fulfillments | Warehouse or 3PL | Warehouse → Shopify | Shipment packed and handed to carrier |
| Tracking numbers | Warehouse or 3PL | Warehouse → Shopify | Label printed |
| Refunds and returns | Shopify for money, warehouse for goods | Both, split by field | Return received, refund issued |
Products and refunds cause most of the trouble. Products are covered by the field table below. For refunds and returns, the money side lives in Shopify: a refund is issued there, and once issued it can’t be cancelled or reversed. The goods side lives in the warehouse: what came back, in what condition, and whether it can be sold again. A good design sends the return receipt from the warehouse to a person or an approval step, and lets that person decide the refund in Shopify. Automating the refund itself is a business decision, not a technical default.
Decide the source of truth per field, not per system
“The ERP is the master” sounds clear until someone fixes a typo in Shopify and the next sync puts the typo back. A field-level table avoids that:
| Field | Owner | Shopify’s role |
|---|---|---|
| SKU, barcode | ERP | Read-only copy |
| Cost | ERP | Read-only copy |
| Title, description, images | Shopify | Owner |
| Price | Shopify or ERP, never both | Owner or copy |
| Available stock per location | WMS or 3PL | Copy, updated by the integration |
| Order status | Shopify | Owner |
| Fulfillment and tracking | Warehouse or 3PL | Updated by the integration |
Stock deserves a closer look. Shopify does not have a single stock number per variant. Its inventory states include on_hand, available, committed, incoming, and held-back states such as damaged and safety_stock. committed is stock set aside for unfulfilled orders, and the Admin API can’t adjust it directly; Shopify manages it through orders. So when the warehouse sends “we have 40 units”, decide whether that means on hand or available, and write it to the matching state. Getting this wrong is a common reason stock drifts by exactly the number of open orders.
If more than one Shopify store shares the same warehouse stock, read inventory across multiple Shopify stores before choosing a design.
Two ways to connect a warehouse
Shopify gives warehouses and 3PLs two broad patterns.
- The warehouse acts as a fulfillment service. Shopify’s fulfillment service apps model this: creating a fulfillment service makes a location for it, merchants send fulfillment requests, and the service accepts or rejects each one, then creates a fulfillment with tracking. Webhook topics such as
fulfillment_orders/fulfillment_request_submittedandfulfillment_orders/cancellation_request_submittedtell the integration when to act. - The integration pulls orders and pushes results. The integration reads new orders, sends them to the WMS or 3PL in its own format, and writes fulfillments and stock back. This is simpler when the warehouse has its own order intake and no concept of accepting requests.
The first keeps a record in Shopify of who was asked to ship what. The second puts that bookkeeping in your integration.
Webhooks, polling and bulk operations
A typical integration uses all three, each for a different job.
| Method | Use it for | Watch out for |
|---|---|---|
| Webhooks | Reacting within seconds to new orders, refunds, product changes | Delivery is not guaranteed; duplicates and out-of-order events happen |
| Scheduled polling | Reconciliation: everything changed since the last run | Rate limits; the default 60-day order window |
| Bulk operations | First load, full catalog sync, large stock updates | Asynchronous; results come back as a file |
Webhooks are for speed. The details of verifying them, answering fast and dropping duplicates are in our integrations checklist.
Polling is for correctness. A job that asks Shopify for orders updated since the last successful run catches anything the webhooks missed. One limit matters for first loads: by default an app sees the last 60 days of orders. Reading older orders, for example to backfill an ERP, needs the read_all_orders scope, which requires Shopify’s approval.
Bulk operations are for volume. Bulk mutations let you upload a JSONL file of inputs and run one mutation over all of them in the background. The file can’t exceed 100MB, an operation must finish within 24 hours, and from API version 2026-01 an app can run up to five bulk mutation operations at once per shop. Shopify can notify you through the bulk_operations/finish webhook, but the docs advise polling the operation’s status as well, because webhook delivery isn’t guaranteed.
Idempotency: writing back without doubling
Retries are normal. A request times out, the integration does not know whether it landed, and it tries again. For stock, that can double an adjustment.
Shopify has two tools for this:
- Idempotency keys. With idempotent requests, a mutation that supports the
@idempotentdirective runs only once for a given key, however many times it is retried. Generate one key per business event (one receipt, one count) and reuse it on retry. In bulk mutations, the key is passed per row. - Compare-and-set for stock.
inventorySetQuantitiesonly updates a quantity if the stored value matches thecompareQuantityyou send. If something else changed the stock in between, the write fails instead of overwriting it, and your integration can re-read and decide. Skipping that check is possible, but Shopify warns it can produce inaccurate quantities under concurrent requests. From API version 2026-04, the docs say an idempotency key is required on this mutation.
Also record where each change came from: Shopify’s inventory mutations accept a referenceDocumentUri, so a stock change can point back to the warehouse receipt or count that caused it.
Failure handling
Every integration fails sometimes. What matters is that failures are kept, visible and retried safely.
- Queue, don’t drop. Put each incoming event on a queue before processing it. If the ERP is down, the queue waits; nothing is lost.
- Retry with backoff, then park. Retry a failed record a few times with growing delays. After that, move it to a “needs attention” list with the error, rather than retrying forever.
- Separate bad data from outages. A missing SKU in the ERP will fail every time; a timeout may succeed in a minute. Treat them differently so one bad order does not block the rest.
- Fetch current state when unsure. If an update arrives for an order the ERP has never seen, fetch the order from Shopify rather than guessing.
- Alert a person, in one place, with the store, the record and the reason.
Reconciliation: proving the two sides agree
Even a well-built integration drifts. Reconciliation is a scheduled comparison that catches it.
| Check | How often | What a mismatch means |
|---|---|---|
| Orders in Shopify vs orders in ERP / WMS for the day | Hourly or daily | An order was missed or duplicated |
| Fulfilled orders vs shipments reported by the warehouse | Daily | Tracking not written back |
| Stock per SKU and location | Daily, or after each count | Drift from manual edits or wrong state |
| Refund totals vs credit notes in the ERP | Daily | Books and Shopify disagree |
| Product fields owned by the ERP | Weekly | Someone edited a read-only field in Shopify |
Report mismatches first. A job that silently overwrites stock hides the real cause. Start with a list for a person to review, and automate only the fixes you have watched it get right.
Off-the-shelf connector or custom integration
Many ERPs and 3PLs offer a ready-made Shopify connector. Start there, and check it against your rules.
| Question | Connector usually fits | Custom usually fits |
|---|---|---|
| Is the data model standard (one SKU = one stock item)? | Yes | No: kits, bundles, one sale using several items |
| One store, one warehouse? | Yes | Several stores or locations with their own rules |
| Do field owners match the connector’s defaults? | Yes | You need per-field ownership it can’t express |
| Can you see what it skipped and why? | It has a clear error log | It hides failures |
| Do refunds need an approval step? | Not needed | A person must decide before money moves |
| Does it use a current API version? | Maintained by the vendor | The vendor lags behind Shopify’s changes |
A connector also does not free you from the platform calendar. Shopify releases a new API version every three months, and the REST Admin API is legacy; whoever runs the integration needs to keep up. Our guides to moving from REST to GraphQL and maintaining a custom Shopify app cover what that involves.
If logic needs to run inside checkout, such as keeping items from one warehouse together, that is a job for Shopify Functions rather than the integration. And if you want an AI tool to prepare stock or refund changes from warehouse data, see building an AI agent with human approval.
Testing before it touches real orders
- On a development store, load your largest catalog with a bulk operation and check the counts on both sides.
- Send the same warehouse receipt twice and confirm stock changes once.
- Change the stock in Shopify between a read and a write, and confirm the stale write is rejected.
- Take the ERP offline for an hour and confirm the queue holds and drains.
- Create a mismatch on purpose and check reconciliation reports it.
FAQ
Should the ERP or Shopify be the source of truth for products?
Decide per field. Commonly the ERP owns SKU, cost and weight, and Shopify owns title, description and images. What matters is that each field has exactly one owner and the integration only writes in that direction.
How do I stop stock updates from being applied twice?
Use an idempotency key per stock event, reused on every retry, and use inventorySetQuantities with compareQuantity so a write based on a stale value fails instead of overwriting newer stock.
Can I import all historical orders into my ERP?
By default an app sees the last 60 days of orders. Older orders need the read_all_orders scope, which Shopify must approve. For large volumes, use bulk operations rather than paging through orders one request at a time.
Should my 3PL be set up as a fulfillment service?
It helps when the 3PL should accept or reject each request and handle cancellation requests before shipping. If the 3PL takes orders through its own intake and only reports shipments, a pull-and-push integration can be simpler.
Do I still need reconciliation if webhooks work?
Yes. Shopify does not guarantee webhook delivery, and manual edits happen on both sides. A scheduled comparison is how you find drift before it becomes an oversell or a wrong invoice.
Getting it built
When a connector can’t express your field owners, your stock rules or your approval steps, a custom integration built around them fits better. Our custom solutions page explains how we approach integrations, and how to brief a developer lists what to send.