Why does my Shopify inventory count never match my warehouse or ERP?
When Shopify, your ERP, and a warehouse system all think they know the real stock count, someone ends up overselling. Here is how to define a real source of truth and stop the drift.
What's going on
You've got inventory living in more than one place, such as Shopify locations, a warehouse management system, maybe a POS or an ERP, and the numbers don't agree. A SKU shows 12 in stock on the storefront but the warehouse says 3, or a transfer between two Shopify locations never shows up on the source system's side. Eventually this turns into an oversold order, an angry customer, and a support ticket nobody can fully explain after the fact.
This is fundamentally an integration design problem, not a Shopify bug. Shopify's location and inventory model is solid on its own: each product variant has an inventory item that can be tracked independently at every location you set up, and Shopify emits webhooks whenever a level changes. The trouble starts when two or more systems each try to be the one true count and push updates to each other, because now every sync is a race condition waiting to overwrite something correct with something stale.
The fix isn't a bigger sync job or more frequent polling. It's deciding, structurally, which system is allowed to say what the real number is, mapping products correctly so updates land on the right SKU at the right location, and building in retries and reconciliation so a missed webhook or a dropped API call doesn't quietly compound into a stockout or an oversell.
Why it happens
Most stores end up with two or more systems each capable of writing inventory counts: Shopify admin itself (staff doing manual counts or adjustments), a warehouse or 3PL system, an ERP, and sometimes a separate POS. Without an explicit rule for which one wins, updates flow in whichever direction the last integration happened to be built, and conflicting writes silently overwrite each other.
SKU mapping is the second common failure point. Integrations that match products by title, variant name, or product ID instead of a stable SKU or barcode break the moment someone renames a variant or a product gets recreated, silently pointing updates at the wrong item or no item at all.
The third is missing error handling: an inventory write can fail for a specific item or location without the overall request itself throwing an obvious error, and webhooks can be missed during outages or rate limiting. Without retries and a reconciliation pass to catch what was missed, small failures accumulate invisibly until a customer notices.
5 ways to fix it
Pick a single system of record and stop syncing bidirectionally
Decide once, in writing, which system (your ERP or OMS, a dedicated inventory platform, or Shopify itself if you only sell through Shopify's own channels) owns the true count. Every other system should only receive pushes from that source, never write back to it except through one clearly defined path, such as returns or damage adjustments. Two systems that both believe they are authoritative and sync in both directions is the single most common cause of phantom stock and oversells.
Use Shopify's current inventory APIs, not whatever your integration happened to be built on years ago
Shopify has been moving inventory management toward its GraphQL Admin API, including mutations for setting absolute quantities and for applying delta adjustments like a sale or a manual recount. If your integration still relies on older REST inventory endpoints, check whether they are still fully supported for your app type, since Shopify has been phasing out REST in favor of GraphQL for newer apps. For stores that only sell through Shopify's own channels, multi-location stock tracking and transfers are handled natively in the admin and do not require a separate integration at all.
Map by SKU or barcode, never by product title or variant name
Build and cache a lookup table that maps your source system's SKU directly to Shopify's internal variant and inventory item identifiers, pulled from the API and refreshed on a schedule. Do not re-derive the mapping from product titles or option names, since renaming a variant on either side can silently break the link. Store Shopify's internal IDs alongside your SKU as a stable foreign key so a rename never breaks the sync.
Subscribe to inventory webhooks and treat manual admin edits as discrepancies, not silent corrections
A large share of drift comes from staff adjusting stock by hand in Shopify admin during a busy shift. Subscribe to inventory-level webhooks so any change Shopify sees gets logged back to your source system for review, instead of letting a manual tweak quietly become the new truth until the next sync overwrites it. Also check the error or user-errors information returned on every write call rather than assuming success from a 200 response, since an update can be rejected for an invalid item or location without the call itself failing outright.
Run a scheduled reconciliation job, do not rely on real-time sync alone
Real-time webhooks and API calls will eventually miss an event, time out, or double-fire. Add an hourly or nightly job that pulls full inventory levels by SKU and location from both systems and flags variances above a threshold, plus a lighter weekly cycle count for your highest-velocity SKUs. This is what actually catches the slow leaks, such as a failed webhook retry, a merged location, or a changed SKU, before they turn into an oversold bestseller.
Bottom line
None of this requires ripping out your current stack. It requires agreeing on ownership, mapping cleanly by SKU, using Shopify's current inventory APIs instead of legacy endpoints, and adding a reconciliation safety net so drift gets caught in hours instead of after a customer orders something you don't have. If you're stitching together more than two systems yourself, such as a POS, a 3PL, a marketplace, and an ERP, a purpose-built inventory sync or middleware app is usually a better use of engineering time than a custom integration, but that's a build-versus-buy call specific to your stack rather than a one-size-fix.
Browse the rest of the problem library, run a free storefront scan to catch issues like this automatically, or email us and we'll work out a custom solution for it.