Skip to content
SystemSyncer
Menu

Guide · Inventory

How to Synchronize Inventory Between Shopify and an ERP

Keep Shopify stock levels aligned with your ERP — choosing which quantity to publish, mapping locations, using compare-and-swap updates, and reconciling drift.

Last reviewed:
Reading time
5 min read
Intermediate

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.

Data flow ERP-to-Shopify inventory flow
  1. ERP stock movement Receipt
  2. Outbox Changed SKUs
  3. Calculator Sellable qty per location
  4. inventorySetQuantities CAS + idempotency key
  5. 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_hand is the total physically stocked at the location. Shopify documents it as the sum of available, committed, reserved, damaged, safety_stock, and quality_control.
  • committed is 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:

  1. Publish on_hand instead of available. If you set on_hand to the ERP’s physical count, Shopify derives available by subtracting its own committed units. This works well when Shopify’s committed quantity mirrors the ERP’s backlog closely.
  2. 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 referenceDocumentUri with 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.
Version-specific behavior

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 available while 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. [1] Manage inventory quantities and states — Shopify
  2. [2] inventorySetQuantities mutation — Shopify
  3. [3] Idempotent requests — Shopify
  4. [4] Bulk operations (queries) — Shopify