FurnitureAxis pushes products and stock to Shopify from a background worker written in C#, on a schedule that also handles the vendor-catalog sync and the Algolia index rebuild. That’s the shape most Shopify integrations start with: something changes in the system of record, and a job pushes the update out on cron.
The part that trips people up is what happens on the other side of that push. Shopify isn’t a shelf you’re stocking and walking away from. Orders come in, other apps adjust stock, staff edit products straight from the admin. A sync that only pushes stops being accurate the moment any of that happens, and it fails quietly, because the push job has no way to know the numbers it’s about to overwrite are already wrong.
Push: on change or cron, and which one is the source of truth
The push side is the easy half, and it’s the half FurnitureAxis has running today. A change happens in the system that owns the data, and a job ships it to Shopify. Two triggers usually run alongside each other: push immediately when a record changes, so the storefront doesn’t sit stale for an hour, and push everything again on a cron sweep, so a missed event or a failed request from the immediate path gets caught on the next cycle.
For inventory specifically, Shopify’s Admin API has a mutation built for exactly this shape. inventorySetQuantities sets an absolute quantity at a location rather than a delta, and Shopify is direct about who should call it: “Only use this mutation if calling on behalf of a system that acts as the source of truth for inventory quantities.” If you’re not the source of truth, the docs point you to inventoryAdjustQuantities instead, which works in deltas and doesn’t assume it’s overwriting the correct number. Getting that choice backward is how a warehouse system and a storefront end up disagreeing about what’s in stock. It also carries compare-and-set logic by default, checking the quantity it expects to find before it writes, and as of API version 2026-04 it requires an idempotency key, so a retried request can’t double-apply.
Listening back: what a webhook actually promises
The other half is hearing what changes on Shopify’s side: an order that decrements stock, a product edited in the admin, an app adjusting a variant. Shopify’s own webhook documentation is unusually blunt about the limits here. Delivery isn’t guaranteed, and the docs say plainly that “your app can miss or mishandle events for other reasons, such as handler failures or downtime.” There’s no ordering guarantee either, within a topic or across topics for the same resource, so a products/update and an inventory_levels/update for the same item can arrive in either order, or with a gap between them you didn’t cause.
Two details from the docs make this workable rather than hopeless. Each delivery carries an X-Shopify-Webhook-Id header, which lets a handler recognize and drop a duplicate instead of applying the same change twice. And Shopify recommends sequencing by the X-Shopify-Triggered-At header or the payload’s updated_at field rather than by arrival order, since arrival order isn’t something you can rely on. A handler that ignores both of those will eventually apply an old update after a newer one, and the storefront will show a number that used to be right.
const seen = await store.has(id);
if (seen) return res.status(200).end();
const current = await store.getUpdatedAt(sku);
if (updatedAt <= current) return res.status(200).end();
await store.set(id, sku, updatedAt);
await applyInventoryChange(sku, quantity);
Reconciliation: the job that trusts neither side
Push and webhooks cover most changes, but “most” is the problem. Shopify’s own guidance on this is the line worth taking seriously: “your app shouldn’t rely on receiving data from Shopify webhooks,” and it recommends a periodic job that fetches the current state directly and reconciles it, rather than trusting event delivery alone. That job is the one piece that doesn’t assume anything arrived. It pulls both sides and diffs them.
Running that diff against the full catalog on a normal request budget would run into Shopify’s rate limits fast. The REST Admin API uses a leaky-bucket limiter, and the GraphQL Admin API prices each query by calculated cost rather than raw request count, both there to stop one integration from starving the rest of a shop’s API traffic. Bulk operations are the documented way around that for large reads: they don’t carry the per-query cost ceiling or the standard rate limit, which is why a full-catalog reconciliation pass is a bulk query and not a loop of individual product lookups.
Where this shows up
None of this is specific to furniture retail. Any retail and POS business running a storefront alongside a warehouse or back-office system hits the same shape: which system says how much stock exists, and what happens when the other one disagrees. The same questions apply to a broader ecommerce build that syncs to Shopify, Amazon, or a marketplace API at the same time, since each of those channels has its own version of the webhook and rate-limit trade-offs above.
The verdict
A one-way push works fine right up until the other side changes without asking, and then it’s just a mirror pointed the wrong way. The push side needs to know whether it’s the source of truth, because that decides which mutation to call. The listening side needs to treat every webhook as possibly late, possibly duplicated, or missing entirely. Underneath both, a reconciliation job that trusts neither side is what catches the gap between what you think you pushed and what Shopify actually has. If you’re wiring an inventory system into Shopify and want the architecture reviewed before the first oversold order shows up, that’s a conversation worth having early. Get in touch and we’ll walk through what your specific system needs.