LUCENTCOMMERCEGET A FREE STORE AUDITFREE AUDIT

INTEGRATIONS · OPS · DATA · 12 JUNE 2026 · 8 MIN READ

Marketplace listings and the inventory they will oversell

You cannot make two buyers on two channels take the same unit safely. The design question is not how to prevent oversell — it is how much you will accept, and who pays for it.

An order moving from storefront to ERP through a queue

Accept that you will oversell and design the size of the problem. A marketplace accepts an order against its own cached copy of your stock and only then tells you, so no amount of sync speed on your side closes the window — there is no distributed transaction across Shopify and a channel you do not control. The two real levers are a buffer held as safety stock, and allocating a fixed quantity per channel instead of sharing one pool. For most mid-market catalogues, allocation is cheaper to build and far more predictable than chasing real-time truth.

IN SHORT

  • A marketplace sells against its own copy of your stock and informs you afterwards, so the race is decided on their side of the integration, not yours.
  • The conflict is rarely Shopify versus your ERP; it is two buyers on two channels within the same second, and no API resolves that after the fact.
  • Allocating a fixed quantity per channel converts an unbounded risk into a known one, at the cost of showing less stock than you have.
  • Shopify documents `safety_stock` as an inventory state, so a buffer against oversell belongs in the data model rather than as an arbitrary subtraction inside an integration.
  • Write authoritative quantities with `inventorySetQuantities` and always include `compareQuantity` — Shopify documents it as updating only if the persisted quantity matches, and warns that bypassing the check "can lead to inaccurate inventory quantities if multiple requests are made concurrently".
  • Webhooks are a signal, not a ledger: Shopify states it "doesn't guarantee ordering within a topic, or across different topics for the same resource" and that delivery is not always guaranteed, so a scheduled reconciliation is mandatory rather than optional.
  • Measure oversell as a rate per channel alongside your cancellation rate; an integration nobody measures is assumed to work until a peak week proves otherwise.

Why the oversell is structural

Trace a single unit through a marketplace sale and the problem becomes obvious. Your integration publishes a quantity. The channel ingests it on its own schedule and caches it behind its own listing infrastructure. A buyer clicks buy. The channel checks its cached number, accepts the order, and some seconds or minutes later tells you about it through an API or a feed. By the time your system learns a unit is gone, the promise has already been made to a customer by a system with no knowledge of what your storefront sold in the meantime.

Nothing on your side of that sequence closes the gap. You can push updates more often, and the channel still sells against whatever it last ingested. You can move from a scheduled feed to an event-driven push, and you have shortened the window rather than removed it. There is no two-phase commit between Shopify and a marketplace, and there is not going to be one, because the marketplace has no incentive to make its checkout wait on your API.

This is worth saying plainly at the start of a project, because a great many marketplace integrations are scoped as though the goal were zero oversell. It is not an achievable goal, and treating it as one produces the expensive version of the build: bidirectional real-time sync, per-event pushes, a queue, retry logic, monitoring for all of it — and still an oversell in the first busy week, now with a larger system to blame.

The achievable goal is different and more useful: keep oversell rare enough that the cost of the cancellations is smaller than the cost of the stock you held back to prevent them. That is an arithmetic question your finance team can actually answer.

Two designs, and the one most people should choose

Shared pool. Every channel sees the same available quantity. It maximises the stock on display and it is what most integrations do by default, because it is what happens if nobody decides otherwise. Every channel races every other channel for the last unit, and the probability of a collision rises with velocity — which means your best-selling lines, on your busiest days, are precisely where it fails.

Allocation. Each channel gets a fixed quantity that only it can sell. Forty on the storefront, fifteen on the marketplace, five held back. A collision is now impossible, because two channels are never competing for the same unit. The cost is visible and bounded: you are showing less stock than you have, and one channel can sell out while another sits on inventory.

For most mid-market merchants we would argue for allocation, and the reason is not technical elegance. Allocation converts an unpredictable, customer-facing failure into a predictable, internally-visible cost. An oversell produces a cancellation, an apology, a refund, a support ticket and a mark against your seller account. An allocation that ran out produces a line on a report that somebody can rebalance next week. Those are not comparable problems, even when the lost margin is similar.

Allocation is also dramatically cheaper to build. You are publishing a number that changes when you decide it changes, rather than maintaining a low-latency pipeline whose whole value proposition is winning a race it cannot reliably win.

The case for a shared pool is real but narrower than people assume: deep stock relative to daily velocity, slow-moving lines, or a catalogue where a stockout costs more than an occasional cancellation. If you hold three hundred of something and sell four a day, share the pool and stop thinking about it.

The buffer belongs in the data model

Whichever design you pick, you will hold something back. The common implementation is a subtraction buried in the integration — publish available - 3 — which works until somebody needs to know why the marketplace shows three fewer than the admin, and the answer is in a repository they cannot read.

Shopify gives you a better place for it. Its inventory model documents safety_stock as one of the states that make up on-hand, alongside available, committed, reserved, damaged and quality control. A buffer expressed as safety stock is visible in the admin, reportable, and adjustable by the operations team who understand why it exists — without a deployment.

Two practical notes. Committed is Shopify's to manage, through orders and fulfilments, so your integration should never be trying to write it. And the buffer is not a constant: the right number for a line selling four a day is not the right number for one selling forty. Review it per line, quarterly, using the oversell rate rather than intuition.

Writing quantities without racing yourself

Everything above concerns races you cannot win. This section is about the ones you can, which is your own systems overwriting each other.

For authoritative writes, inventorySetQuantities sets the available or on_hand quantity to an absolute value. Always send compareQuantity with it. Shopify documents the mutation as updating only if the persisted quantity matches the value you supplied, and the corresponding escape hatch, ignoreCompareQuantity, carries an explicit warning that it "can lead to inaccurate inventory quantities if multiple requests are made concurrently". The documentation's own recommendation is to always include the compare value.

A failed compare is not an error to swallow. It means the world changed between your read and your write — which is exactly the condition you built the integration to notice. Re-read, re-decide, retry. An integration that logs those and carries on is telling you it is losing writes.

For increments and decrements — a return coming back, a count correction — inventoryAdjustQuantities applies a delta rather than an absolute. Deltas are the right instrument for changes you know the size of and the wrong one for periodic sync, because a missed delta is silently permanent while a missed absolute is corrected by the next write.

Batch deliberately, too. Array inputs are documented as capped at 250 items across every Shopify API, and the GraphQL Admin API meters by calculated query cost through a leaky bucket rather than by request count. Both facts point the same way: fewer, larger, well-shaped calls beat a per-SKU loop, particularly during the overnight catch-up when everything runs at once.

Webhooks tell you something happened, not what is true

Most marketplace integrations subscribe to inventory webhooks — INVENTORY_LEVELS_UPDATE and its siblings, under the read_inventory scope — and treat the payload as the new state of the world. Shopify's own documentation says not to.

Ordering is not guaranteed: "Shopify doesn't guarantee ordering within a topic, or across different topics for the same resource." So two updates to the same item can arrive in the wrong order, and a naive handler will happily write the older one last. Use the X-Shopify-Triggered-At header or the updated_at field to sequence them, and discard anything older than what you have already applied.

Delivery is not guaranteed either — the documentation states plainly that webhook delivery "isn't always guaranteed, and your app can miss or mishandle events". Duplicates need handling as well; X-Shopify-Webhook-Id is what you deduplicate on.

Which leads to the piece that is invariably cut from the first version of the project and added after the first incident: a reconciliation job. Shopify recommends it directly. On a schedule, read the authoritative quantities, compare them with what each channel currently believes, and correct the differences. Log every correction, because the volume of corrections is the honest health metric for the whole integration — a number that starts climbing is telling you something upstream broke weeks before anyone notices a customer complaint.

What to build, in order

The sequence matters, because the first two items frequently make the rest smaller than anyone expected.

  • Decide shared pool or allocation, per product line rather than for the whole catalogue. Fast movers get allocation; deep, slow lines can share.
  • Express the buffer as safety stock in Shopify so operations can see and change it without a release.
  • Write authoritative quantities with inventorySetQuantities and compareQuantity, and treat a failed compare as a real event — re-read and retry rather than logging and continuing.
  • Subscribe to inventory webhooks as a signal only, sequenced on the triggered-at timestamp and deduplicated on the webhook id.
  • Build the reconciliation job in the same sprint as the sync, not after the first incident. Count and publish its corrections.
  • Instrument oversell as a rate per channel, next to your cancellation rate. Read your channel's seller performance policy before choosing a buffer size — cancellations are counted against sellers and the thresholds differ by marketplace.
  • Review buffers and allocations quarterly against those rates. This is an operations routine, not an engineering task, and it needs an owner with a name.

What we would push back on

The request that arrives most often is real-time bidirectional sync across every channel, and we will usually argue against the full version of it. It is the most expensive build available, it is the hardest to test, and its selling point — eliminating oversell — is something it cannot deliver, because the decisive moment happens inside a system you have no API into.

What we would build instead is the smaller thing: clear ownership of the authoritative number, allocation on the lines where velocity makes collisions likely, a buffer somebody adjusts monthly, and reconciliation you can read a graph of. It handles the same volume, it fails in ways you can explain to a customer, and there is less of it to maintain in year two.

If, after measuring, the oversell rate genuinely justifies a lower-latency pipeline on a specific channel, build it for that channel. That is a very different project from rebuilding everything to a standard your least important marketplace does not require — and it is the shape of [integration work](/services/integrate) that survives contact with a peak season.

Questions this raises

How do you sync inventory with marketplaces?

Establish one authoritative quantity per location, publish it to each channel, and reconcile on a schedule. Write to Shopify with `inventorySetQuantities` and `compareQuantity` so concurrent writes cannot silently overwrite each other, use inventory webhooks as a signal to re-read rather than as the new state, and hold a buffer as safety stock. On fast-moving lines, allocate a fixed quantity per channel instead of sharing one pool.

Why do marketplaces oversell even with a real-time integration?

Because the marketplace sells against its own cached copy of your stock and tells you afterwards. Your integration learns a unit is gone after the buyer has already been given a confirmation. There is no transaction spanning Shopify and a channel you do not control, so faster syncing narrows the window without closing it.

Should each channel have its own inventory allocation?

On anything fast-moving, yes. Allocation makes a collision impossible because no two channels compete for the same unit, and it turns an unpredictable customer-facing failure into a predictable internal cost — a channel that sells out rather than an order you have to cancel. Shared pools are reasonable where stock depth is large relative to daily velocity.

Where should an inventory buffer live?

In Shopify's inventory model, as safety stock — it is one of the documented quantity states that make up on-hand. Putting the buffer there makes it visible in the admin and adjustable by the operations team. A subtraction hidden inside the integration works until somebody asks why the marketplace shows three fewer than the admin, and the answer is in code they cannot read.

Are Shopify inventory webhooks reliable enough to sync from?

Not on their own. Shopify documents that it does not guarantee ordering within a topic or across topics for the same resource, and that delivery is not always guaranteed. Sequence events on the triggered-at timestamp, deduplicate on the webhook id, treat each event as a prompt to re-read rather than as truth, and run a scheduled reconciliation — which Shopify itself recommends.

Should we use set or adjust for inventory quantities?

Set for periodic authoritative sync, adjust for known increments and decrements such as a return or a count correction. The distinction matters under failure: a missed absolute write is corrected by the next one, whereas a missed delta is permanently wrong and nothing downstream will ever notice.

NEXT STEP

Free store audit

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