LUCENTCOMMERCEGET A FREE STORE AUDITFREE AUDIT

OPERATIONS · ERP · INTEGRATIONS · DATA · 11 NOVEMBER 2025 · 9 MIN READ

Inventory truth: the number three systems disagree about

Shopify, the ERP and the warehouse each show a different figure, and every integration project tries to make them agree. The fix is deciding which one is allowed to be right.

An order moving from storefront to ERP through a queue

Whichever system physically counts the shelf — for most merchants that is the warehouse or 3PL, not the ERP and not Shopify. One system owns on-hand per location, writes it to Shopify as an absolute value rather than a delta, and everything else reads. The part people get wrong is assuming this makes Shopify a passive mirror: it does not, because Shopify owns the committed state through orders and fulfilments, and its documentation is explicit that apps cannot modify committed quantities. So the correct model is not one source of truth but a division of ownership — the warehouse owns what is on the shelf, Shopify owns what has been promised to a customer, and available is the arithmetic between them. Integrations that fight over available instead of respecting that split are the ones that oversell.

IN SHORT

  • Shopify documents eight inventory states: incoming, on hand, available, committed, reserved, damaged, safety stock and quality control.
  • On hand "equals the sum of inventory quantities in the following states: available, committed, reserved, damaged, safety stock, quality control" — so available is a component of on hand, not a separate pool.
  • Apps cannot directly modify committed quantities; Shopify manages them through orders, draft orders and fulfilments.
  • Write authoritative values with `inventorySetQuantities`, which sets available or on hand using absolute values; Shopify documents it as being for systems acting as the authoritative source.
  • That mutation supports compare-and-set through `compareQuantity`, and Shopify discourages `ignoreCompareQuantity` because bypassing the check risks accuracy problems under concurrent load.
  • Use `inventoryAdjustQuantities` for increments and decrements — corrections and ongoing adjustments — rather than as the main sync mechanism.
  • Safety stock is a documented state, which means a buffer against oversell belongs in the data model rather than in an arbitrary subtraction inside your integration.

Why three systems disagree, and why that is normal

The three numbers on the screen are usually all correct. They are answering different questions.

The warehouse knows what is on the shelf, as of the last time somebody scanned something. The ERP knows what it believes it owns, which includes goods in transit, allocations against purchase orders, and stock it has committed to a wholesale customer nobody told the website about. Shopify knows what it can sell, which is what it has been told exists minus what customers have already bought and it has not yet shipped.

The instinct is to make all three show the same figure. That instinct is wrong, and chasing it is how integration projects consume six months. Three systems with different jobs will legitimately show different numbers; what you need is not agreement but a defined relationship — which number derives from which, in which direction, and who is allowed to write.

Shopify's own model shows how much structure is already available. The documentation lists eight quantity states: incoming, on hand, available, committed, reserved, damaged, safety stock and quality control. And it defines the relationship arithmetically — on hand "equals the sum of inventory quantities in the following states: available, committed, reserved, damaged, safety stock, quality control". Most disagreements dissolve once the two sides work out they were comparing on hand against available.

The division of ownership that actually works

Ownership is not one decision. It is three, and separating them is most of the design work.

On hand belongs to whoever counts. The physical count is an observation of the real world, and only the system in the room can make it. For a merchant using a 3PL, that is the 3PL. For a merchant with its own warehouse and a WMS, that is the WMS. The ERP is usually the wrong answer here, and it is the one most often proposed — because the ERP is the system finance trusts, and trust is not the same as knowledge. An ERP that receives its own counts from the warehouse is a relay, and a relay should not be the source.

Committed belongs to Shopify, and you have no choice. The documentation is direct: apps cannot directly modify committed quantities, because Shopify manages them through orders, draft orders and fulfilments. This is a feature. The commitment is created at the moment of sale, in the system where the sale happened, without a round trip. Any design that tries to calculate commitments elsewhere and push them in is fighting the platform and will lose during a busy hour.

Available is arithmetic, not an opinion. Once on hand is owned and committed is owned, available follows. The failure mode is two systems both writing available on their own schedules, each computing it from a partial view, overwriting each other in whatever order the queue happened to run.

The reason this matters commercially is specific: available is what gates the sale. Get its inputs wrong and you either oversell — cancellations, refunds, a support queue — or you undersell, which is worse because it is invisible. Nobody files a ticket about the thing they could not buy.

Absolute values, not deltas

The single biggest reliability decision in an inventory integration is whether you send "there are 14" or "subtract 2".

Deltas are seductive because they are small and they seem to compose. They do not survive the real world. A delta applied twice is wrong, a delta lost is wrong, and both are permanent — there is no subsequent message that corrects them, so the error persists until somebody counts the shelf. Retries, at-least-once delivery and out-of-order processing are normal conditions in any queue, which means duplicate and dropped messages are not edge cases.

Absolute values are self-healing. Send "there are 14" and it does not matter whether the previous message arrived, arrived twice, or arrived late — the next message brings the system back to correct. Shopify supports this directly: inventorySetQuantities sets the available or on-hand quantity using absolute values, and the documentation frames it as the mutation for a system acting as the authoritative source.

It also gives you a defence against the one race condition that matters. The mutation accepts compareQuantity, which only applies the update if the persisted quantity matches what you believed it was, and fails with an error otherwise. That turns a silent overwrite into a visible conflict you can re-read and retry. There is an ignoreCompareQuantity escape hatch, and Shopify discourages it because bypassing the check risks accuracy problems under concurrent load. Use the escape hatch for a genuine reset after a stocktake, not because a conflict error appeared in your logs and you wanted it to stop.

inventoryAdjustQuantities still has a place — Shopify describes it for incrementing or decrementing across the adjustable states, which is what a correction or a damage write-off actually is. The rule of thumb: the sync sets, the exception adjusts.

Buffers belong in the model, not in the integration

Almost every merchant wants a cushion — publish slightly less than you have, so a counting error does not become an oversell. Almost every integration implements this by quietly subtracting a number before writing to Shopify.

That is where it goes wrong. The subtraction is invisible to everyone outside the integration, so the warehouse sees 20, Shopify shows 18, and now the two systems disagree by a rule that lives in code nobody reads. Six months later someone changes it from 2 to 3 and the discrepancy report nobody trusted gets slightly worse.

Shopify has a state for this. Safety stock is one of the eight documented quantity states, and it is one of the states an app can adjust. Put the buffer there, and the arithmetic stays honest: on hand is still the physical count, safety stock holds the cushion explicitly, and available comes out lower for a reason anyone can see in the admin. Damaged and quality control work the same way for stock that physically exists but must not be sold — a returns pile awaiting inspection is not a counting error, it is quality control, and modelling it as one makes the difference explicable.

The general principle: every unit of the gap between what is on the shelf and what you will sell should be attributable to a named state. A discrepancy you can explain is an operational decision. A discrepancy you cannot is a bug you will spend a week looking for.

Reconciliation, because sync alone is not enough

Even a well-built integration drifts. Messages fail during a deploy, a warehouse adjusts something manually, a webhook is missed, an order is edited in a way nobody modelled. Drift is not a sign of a bad integration; the absence of a way to detect it is.

So run a reconciliation independently of the sync. Pull the authoritative on-hand figures for each location, pull what Shopify holds, compare, and report. Not fix — report, at least at first. A job that silently corrects discrepancies is a job that hides the pattern that would have told you which SKUs, which locations or which hours are producing them.

Set a threshold that triggers a human. Small drift on a fast-moving SKU is timing; a 40-unit gap on something that sells twice a week is a bug. And record the reconciliation results over time, because the useful signal is the trend. A discrepancy count that is flat is a system with a known error rate. One that is climbing is a system that broke on a specific day, and having the history tells you which day.

  • One system owns on hand per location — the one that counts the shelf.
  • Shopify owns committed; do not attempt to calculate it elsewhere.
  • Writes are absolute values with compare-and-set, not deltas.
  • Buffers live in safety stock, damaged or quality control — never in a hidden subtraction.
  • A scheduled reconciliation reports drift, with a threshold that reaches a person.
  • Every location maps explicitly; an unmapped location is stock nobody is selling.

What we would push back on

Three proposals come up in nearly every inventory project, and each is more expensive than the problem it solves.

"Make the ERP the source of truth for stock." Usually wrong, for the reason above: the ERP is a relay for physical counts, not an observer of them. Finance can own valuation, cost and ownership while the warehouse owns the count. Those are different questions and one system does not have to answer both.

"Buy an inventory platform to sit in the middle." Sometimes justified — genuinely multi-channel merchants with several fulfilment points do reach this — but it is frequently bought to avoid making the ownership decision, and it cannot avoid it. A middle layer with no agreed owner produces a four-way disagreement instead of a three-way one, and adds a subscription.

"Sync every minute." Frequency is not the fix for a correctness problem. If the writes are deltas, faster syncing propagates errors faster; if they are absolute with compare-and-set, an hourly cadence with event-driven updates on the SKUs that move is usually enough. The stores that oversell are almost never the ones that synced too slowly. They are the ones where two systems both thought they owned the same number.

Questions this raises

Which system should own inventory truth?

The one that physically counts the stock — the warehouse or 3PL for most merchants. It owns on hand per location and writes absolute values to Shopify. The ERP receives its counts from the same place, so making it the source adds a relay without adding knowledge; let it own valuation and ownership instead.

Can an integration set Shopify's committed quantity?

No. Shopify documents that apps cannot directly modify committed quantities, because it manages them through orders, draft orders and fulfilments. That is the correct division of labour: the commitment is created where the sale happens, without waiting for an external system to confirm it.

Should inventory updates be sent as deltas or absolute values?

Absolute values. A delta applied twice or lost once is permanently wrong, and duplicate or out-of-order delivery is normal in any queue. `inventorySetQuantities` sets available or on hand using absolute values and is documented for systems acting as the authoritative source, so the next message always restores correctness.

How do I stop two systems overwriting each other?

Use the compare-and-set support on `inventorySetQuantities`: pass `compareQuantity` and the update only applies if the persisted quantity still matches, failing with an error otherwise. Shopify discourages `ignoreCompareQuantity` because skipping the check risks accuracy problems under concurrent load — reserve it for a deliberate reset after a stocktake.

Where should an oversell buffer live?

In the safety stock state, which Shopify documents as one of the eight quantity states and one an app can adjust. Subtracting a hidden number inside the integration makes the warehouse and Shopify disagree by a rule nobody can see. Damaged and quality control serve the same purpose for stock that exists but must not be sold.

How often should inventory sync?

Less often than most people assume, once the writes are correct. Absolute writes with compare-and-set plus event-driven updates on fast movers handle the risk; syncing every minute propagates errors faster if the design is wrong and changes little if it is right. Oversells come from contested ownership, not from cadence.

NEXT STEP

Free store audit

A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.