Short answer
Make the ERP (or warehouse system) the single source of truth for physical stock and publish a sellable quantity to Shopify per SKU and location. Send changes as events from the ERP, write them with Shopify's inventorySetQuantities mutation using compare-and-swap and an idempotency key, and run a full reconciliation regularly. Account for orders Shopify has accepted but the ERP has not yet seen, or you will oversell.
- ERP stock movement Receipt
- Outbox Changed SKUs
- Calculator Sellable qty per location
- inventorySetQuantities CAS + idempotency key
- Shopify Inventory levels
The ERP publishes stock changes; the integration converts them into a sellable quantity per Shopify location.
Inventory sync looks simple — copy a number from one system to another — and is the workflow most likely to cause visible customer problems when it is wrong. Overselling leads to cancellations and complaints; underselling leaves revenue on the table. This guide explains the model, the documented Shopify mechanics, and the design decisions that prevent both.
Understand Shopify’s inventory model
Shopify tracks inventory per inventory item (one per variant) per location, through inventory levels. Each level holds quantities in several named states. Shopify documents these states: incoming, on_hand, available, committed, reserved, damaged, safety_stock, and quality_control.
Two relationships matter for integration:
on_handis the total physically stocked at the location. Shopify documents it as the sum ofavailable,committed,reserved,damaged,safety_stock, andquality_control.committedis stock allocated to orders that are not yet fulfilled. Shopify manages it through order creation and fulfillment; it cannot be adjusted or moved through the GraphQL Admin API.
What the storefront sells against is available. Your integration’s job is to make available reflect what the business can actually ship.
Decide what number to publish
The ERP usually knows physical stock. The figure you publish to Shopify should be a sellable quantity, typically:
sellable = erp_on_hand
− erp_allocated_to_other_channels
− erp_reserved_or_quarantined
− orders_accepted_by_shopify_but_not_yet_in_erp
− safety_buffer
The fourth term is the one most teams miss. Between the moment a customer checks out and the moment the order reaches the ERP, Shopify has already reduced available and the ERP still counts the units as free. If your sync then writes the ERP’s number into available, it undoes the sale and the units can be sold again.
There are two common ways to handle this:
- Publish
on_handinstead ofavailable. If you seton_handto the ERP’s physical count, Shopify derivesavailableby subtracting its owncommittedunits. This works well when Shopify’s committed quantity mirrors the ERP’s backlog closely. - Subtract the in-flight orders yourself. Track orders exported to the ERP versus orders still pending in your queue and subtract unexported quantities before writing
available.
Pick one approach per location and document it. Mixing them causes double-counting.
Map locations
Map each ERP warehouse (or stock site) to a Shopify location in a configuration table. Decide what to do with ERP warehouses that should not be sold from online (for example, a returns warehouse) and with Shopify locations that have no ERP equivalent (for example, a pop-up store).
If several ERP warehouses feed one Shopify location, sum their sellable quantities before writing.
Send changes as events, reconcile in bulk
A good design has two paths:
- Event path. When stock changes in the ERP, emit an event for the affected SKU and warehouse (through an outbox table, change data capture, or a polling query on a “last changed” column). The integration recalculates the sellable quantity for that SKU and writes it to Shopify within seconds or minutes.
- Reconciliation path. On a schedule, compare every SKU’s published quantity with Shopify’s current levels and correct differences. For large catalogs, Shopify’s bulk operations let you export inventory levels as a JSONL file instead of paging through them.
Debounce the event path. A busy warehouse can emit dozens of movements for the same SKU in a minute; collapse them and write only the latest quantity.
Write with compare-and-swap and idempotency
Shopify’s inventorySetQuantities mutation sets available or on_hand to an absolute value. It supports compare-and-swap: you pass the quantity you expect Shopify to currently hold, and the mutation fails if the actual value is different. Shopify recommends always passing the expected current quantity. From API version 2026-04 this is done with changeFromQuantity (earlier versions used compareQuantity), and as of 2026-04 the mutation requires an idempotency key supplied through the @idempotent directive.
mutation SetStock($input: InventorySetQuantitiesInput!, $idempotencyKey: String!) {
inventorySetQuantities(input: $input) @idempotent(key: $idempotencyKey) {
inventoryAdjustmentGroup {
reason
referenceDocumentUri
changes { name delta quantityAfterChange }
}
userErrors { code field message }
}
}
{
"input": {
"name": "on_hand",
"reason": "correction",
"referenceDocumentUri": "erp://stock-movement/WH1/884213",
"quantities": [
{
"inventoryItemId": "gid://shopify/InventoryItem/30322695",
"locationId": "gid://shopify/Location/124656943",
"quantity": 42,
"changeFromQuantity": 40
}
]
},
"idempotencyKey": "6f1c2a7e-0d7b-4b8e-9a51-3c1f0e2a9b77"
}
How to use these features well:
- Generate the idempotency key per logical change, store it with the job, and reuse it on retries. A new key on every retry defeats the purpose.
- On a compare-and-swap failure, re-read the current Shopify quantity, recalculate, and try again. A mismatch means something else changed the level — often a sale.
- Populate
referenceDocumentUriwith the ERP movement or job ID. It shows up in Shopify’s inventory history and makes support investigations much faster. - Batch quantities in one mutation where possible, but stay under the documented 250-item limit for input arrays.
The changeFromQuantity and required idempotency key behavior described here applies to API version 2026-04 and later. If you pin an older version, check the mutation reference for that version.
Handle multi-channel stock
If the same stock sells on Shopify, a marketplace, and B2B channels, the ERP should be the allocation point. Each channel gets its own sellable quantity calculation, and the safety buffer per channel absorbs sync latency. A shared pool with no buffers works only if every channel syncs in near real time.
Error handling
| Situation | Response |
|---|---|
| Compare-and-swap mismatch | Re-read and recalculate; retry a limited number of times |
userErrors for unknown item or location |
Data problem: alert and park the job |
| Throttled | Back off and retry; reduce batch frequency |
| ERP unavailable | Pause the event path; reconciliation catches up later |
Never “fix” a failed inventory write by setting stock to zero or to a large number. Leave the last known good value and alert.
Security considerations
- Inventory write access lets an attacker or a bug take your whole catalog offline or oversell it. Restrict the scope to the integration app and protect its token.
- Add a sanity check that rejects writes changing a quantity by more than a configured threshold (for example, a jump from 5 to 50,000) without manual approval.
Testing
- Place a Shopify order and trigger an ERP stock update for the same SKU before the order reaches the ERP. Confirm the units are not re-released.
- Run two concurrent updates for the same SKU and confirm compare-and-swap prevents a lost update.
- Retry a mutation with the same idempotency key and confirm it is not applied twice.
- Run a full reconciliation against a store with deliberately wrong quantities and review the correction report before enabling automatic fixes.
Common mistakes
- Writing ERP on-hand into Shopify’s
availablewhile orders are still in flight. - Ignoring compare-and-swap (or opting out of it) to “make errors go away.”
- Generating a new idempotency key on every retry.
- No mapping for new warehouses, so stock silently never publishes.
- Syncing every movement individually instead of collapsing to the latest quantity per SKU.
Summary
Treat the ERP as the owner of physical stock, publish a deliberately calculated sellable quantity per SKU and location, and account for orders still in flight. Use inventorySetQuantities with compare-and-swap and a stable idempotency key, debounce event-driven updates, and reconcile the full catalog regularly. Most overselling problems disappear once the in-flight order gap is handled explicitly.
Frequently asked questions
Should Shopify or the ERP be the source of truth for inventory?
In most businesses with an ERP, the ERP or warehouse system should own physical stock because receipts, transfers, and adjustments happen there. Shopify then holds a published, sellable quantity derived from ERP data.
Why does my store still oversell even though inventory syncs every few minutes?
Usually because the quantity you publish does not subtract orders that Shopify has accepted but the ERP has not processed yet. When the next sync overwrites Shopify's count with the ERP's on-hand figure, those sold units become available again.
Can I update the committed quantity in Shopify from my ERP?
No. Shopify documents that the committed state is managed by Shopify through actions such as creating and fulfilling orders, and cannot be adjusted or moved through the GraphQL Admin API.
Is real-time inventory sync necessary?
Not always. Fast-moving or low-stock items benefit from near-real-time updates. For large catalogs with stable stock, a frequent batch combined with a safety buffer is often enough and much cheaper in API usage.
Sources and further reading
Platform-specific statements on this page were checked against these sources on . Vendor APIs change; confirm details in the current documentation before you build.
- [1] Manage inventory quantities and states — Shopify
- [2] inventorySetQuantities mutation — Shopify
- [3] Idempotent requests — Shopify
- [4] Bulk operations (queries) — Shopify
Related reading
- Concepts What Is Inventory Reconciliation? What inventory reconciliation is, why stock drifts between an online store and an ERP, how to compare and correct quantities safely, and how often to reconcile.
- Guides How to Map Ecommerce Products to ERP Articles and SKUs Design product and variant mapping between an ecommerce platform and an ERP — SKU strategy, variants, bundles, units of measure, cross-references, and validation.
- Guides How to Synchronize Shopify Orders with an ERP How to export Shopify orders to an ERP reliably — choosing the trigger, mapping lines, taxes, discounts and payments, handling edits, and avoiding duplicates.
- Guides How to Implement Retries and Idempotency A practical implementation of retries with exponential backoff and jitter, plus idempotency keys, so ecommerce-ERP integrations can retry safely without side effects.
- Comparisons Real-Time vs. Scheduled Synchronization When ecommerce-ERP data should sync in real time and when a schedule is better — compared by workflow, cost, complexity, rate limits, and failure behavior.
- Platforms Shopify ERP Integration How Shopify connects to an ERP — the GraphQL Admin API, webhooks, inventory states, fulfillment orders, rate limits — plus the sync patterns and pitfalls that matter.