Available is not sellable
When a storefront shows a unit as available, that number is already a summary of several inventory states — some of which never emit a webhook. On commerce work where Shopify owns the catalog and something else has to stay honest about what can still be sold, treating inventory_levels/update as a complete ledger is how you oversell quietly and discover it in fulfillment.
Available is a summary, not a ledger
Shopify models inventory across several mutually exclusive states. Available and on_hand are the ones most storefronts and sync jobs stare at. Committed, reserved, damaged, safety stock, and quality control sit alongside them. A unit that moved into committed for an open checkout is no longer sellable in practice, even if your last webhook still described a healthier available count.
That distinction matters the moment a second system cares about stock. A florist's ops pipeline, a marketplace sync, a warehouse feed — anything that decides whether another channel can still take an order — needs the sellable quantity, not the last available event it happened to receive.
So the first modeling decision is to stop treating available as the truth and start treating it as one projection of a ledger you do not fully own.
The webhook only speaks for some states
Shopify's inventory_levels/update webhook fires when available or on_hand change. Movements into committed, reserved, damaged, safety_stock, and quality_control can stay silent. A listener that updates a local cache from that webhook alone will drift by construction: the platform knows a unit is spoken for; your sync does not.
This is the same shape as order webhooks, with a worse failure mode. For orders, silence means a missing ticket. For inventory, silence means you keep selling a unit that is already committed elsewhere. The customer sees success. The shop discovers the conflict when someone tries to pick the stem.
At-least-once delivery still applies — duplicates and out-of-order updates are normal — but the harder bug is the update that never arrives because the platform never emitted it.
Listen for speed, poll for truth
The pattern that holds is hybrid. Use the webhook to move quickly when available or on_hand actually change. Run a reconciliation pull on a schedule against the Admin inventory APIs, and treat that pull as authoritative for the states the webhook skips.
Idempotency belongs on the inventory item and location pair, not on the webhook delivery id. Two events that describe the same SKU at the same location should collapse to one local row. Absolute quantities from a reconciliation beat replaying a stale delta if you cannot prove order.
We already run this instinct on Al's Flowers for orders: acknowledge early, process idempotently, and periodically diff Shopify against local records. Inventory needs the same humility — the event stream is a hint, the pull is the audit.
Decide who owns the count before you sync
Sync architecture fails when every channel believes it can write absolute stock. Pick one system of record. For a single Shopify storefront with light ops, Shopify can remain that record and your side systems should mirror sellable quantity outward. For a warehouse or ERP that receives purchases and adjustments first, the platform becomes a destination you update — carefully, and with the same reconciliation loop in reverse.
Provale Cup keeps the sellable object on Shopify and the explanatory content in Sanity. That seam works because neither system pretends to own the other's facts. Inventory sync wants the same clarity: one place invents the count; everyone else renders or reserves against it.
If two writers both set absolute available without a version or comparison stamp, last-write-wins will eventually invent stock. Prefer adjustments with a known baseline, or accept that reconciliation will overwrite optimistic local guesses.
What we check on a commerce engagement now
During scoping we ask which inventory states the business actually uses, which channels can sell the same SKU, and what happens when those channels disagree. If the honest answer is "we listen to the inventory webhook and hope," we put reconciliation on the critical path before launch traffic makes the gap expensive.
We also ask what sellable means operationally. A stem reserved for a wedding order is not available for the web store. Damaged stock is on hand in a warehouse sense and unsellable in a storefront sense. If those states live only in someone's head, no sync job can encode them.
The Catalog API and Storefront APIs will happily report availability derived from the platform's ledger. Your job is to keep that ledger honest across the systems that change it — not to cache the last webhook and call it inventory.
Questions
- Why isn't the inventory webhook enough?
- It fires for available and on_hand changes, not for every state movement. Committed, reserved, damaged, safety stock, and quality control can change without a delivery — so a webhook-only cache drifts while the platform still knows the unit is spoken for.
- What should reconciliation look like?
- A scheduled Admin API pull of inventory for the SKUs and locations you sell, keyed by inventory item and location, upserted idempotently. Use it to correct absolute quantities; treat webhooks as a fast path, not the ledger.
- Who should own the stock count?
- One system of record. Either Shopify owns sellable quantity and side systems mirror it, or a warehouse/ERP owns receipts and adjustments and Shopify is updated as a destination. Two writers setting absolute available without a baseline will invent stock.