Sync Cadence Controls Staleness. Three Write-Path Decisions Control Correctness.
- An oversell in a NetSuite-to-Shopify sync is usually a lost update, not a lag. The connector reads a quantity from NetSuite, a buyer commits stock in Shopify a few seconds later, and the connector then writes the value it read — putting the committed units back on sale. Shortening the interval shrinks that window; it never closes it.
- Oracle documents that an item’s Last Modified Date “updates only when you update the item record”, while Last Quantity Available Change “updates when you enter or edit an inventory-affecting transaction”. An incremental sync filtered on the first field returns an empty set after every fulfilment and reports success.
- Shopify’s
committedstate cannot be written through the Admin API at all, andon_handis defined as the sum ofavailable,committed,reserved,damaged,safety_stockandquality_control. Which of the two writable states you target changes which failure you get. - Passing
compareQuantityoninventorySetQuantitiesmakes a stale write fail instead of applying. Shopify warns that opting out of the check “can lead to inaccurate inventory quantities if multiple requests are made concurrently”. - From Shopify API version 2026-04 the
@idempotentdirective is mandatory on 17 mutations including every inventory write. A connector pinned to an older version breaks at the upgrade, not before.
@idempotent directive is mandatory on inventory writes (Shopify changelog)getItemAvailability call before NetSuite returns an error (Oracle)“How often should we sync?” is the wrong first question. Cadence decides how stale a number is allowed to get. It does not decide whether the number written to Shopify is correct, and the failure that costs money — selling a unit that is already committed to another order — is caused by how the write is performed, not by how often. A connector that writes absolute quantities without a compare-and-set check will oversell at a 30-second interval and at a 15-minute interval; the faster one just does it less often per order.
This post specifies the three write-path decisions that determine accuracy, then returns to cadence as what it actually is: a cost decision, bounded by NetSuite’s account concurrency pool and Shopify’s points-per-second budget rather than by any general multiplier. Every claim is checked against Oracle’s NetSuite help and shopify.dev in August 2026. Shopify ships a new API version each quarter and NetSuite rolls out releases in phases, so version-pin anything you take from here. The wider set of failure modes on this pairing is collected in our Shopify and NetSuite integration guide library.
Contents
- An oversell is a lost update, not a lag
- Last Modified Date does not move when stock moves
- Which Shopify state you write decides which failure you get
- compareQuantity turns a silent oversell into a failed mutation
- From version 2026-04 the idempotent directive is mandatory
- Round-trip time, not poll interval, sets the incoherence window
- What actually limits how often you can sync
- When a webhook fast-path is justified
- The decision framework
- Where this framework does not apply
An oversell is a lost update, not a lag
The oversell mechanism in a scheduled NetSuite-to-Shopify inventory sync is a lost update: the connector writes a quantity it read before a change it never saw. Lag is what makes the window; the absolute write is what turns the window into a wrong number.
Trace one cycle. The job reads NetSuite and gets a quantity available of 7 for a SKU. Eight seconds later a buyer checks out 2 units on Shopify. Shopify moves those units into the committed state, so the storefront now shows 5 available. Twenty seconds after that the job completes and writes available = 7, because 7 is what NetSuite said when it was asked. The two committed units are back on sale, and the next buyer can purchase stock that is already allocated to an order.
The size of the error is exactly the quantity committed between the read and the write, and it recurs every cycle. Halving the interval halves the exposure per cycle and doubles the number of cycles, which is why teams who shorten the interval often report that overselling did not improve. The defect is not in the schedule. It is in writing an absolute value that was already stale when it was computed.
The diagram below shows the sequence and the window it opens.
Last Modified Date does not move when stock moves
A NetSuite incremental sync filtered on an item’s Last Modified Date will not detect stock movements. Oracle states the behaviour directly in its inventory items documentation: “Lastmodifieddate updates only when you update the item record”, while “Lastquantityavailablechange updates when you enter or edit an inventory-affecting transaction”.
The two fields answer different questions. Editing an item’s description, price or custom fields moves Last Modified Date. Posting a sales order, item fulfilment, item receipt or inventory adjustment moves Last Quantity Available Change and leaves Last Modified Date alone. A connector that pages items WHERE lastmodifieddate > :cursor is asking which item records were edited, which is not the question it needs answered.
This failure is silent in the worst way. The query succeeds, returns zero rows, and the job logs a clean run. Monitoring built on error rates and job completion sees a healthy sync while the storefront quietly diverges from the warehouse for weeks. Detection needs a different signal: compare the number of items the sync touched in a window against the number of inventory-affecting transactions posted in the same window, and alert when the first is zero and the second is not.
Oracle’s SuiteTalk getItemAvailability operation exposes the same field as a request filter, lastQtyAvailableChange, returning “only items with quantity available changes recorded as of the specified date”. That operation supports up to 10,000 records per call and errors above it. Note the trajectory before building on it: Oracle’s SOAP removal plan means new integrations should be on REST web services with OAuth 2.0, so treat the field name as the portable part and the query surface as replaceable.
Which Shopify state you write decides which failure you get
Shopify tracks inventory as eight named states, and only two of them are valid targets for inventorySetQuantities: available and on_hand. The choice is not cosmetic, because Shopify derives one from the other and maintains part of the calculation itself.
Shopify’s documentation defines the relationship precisely: “The on_hand state equals the sum of inventory quantities in the following states: available, committed, reserved, damaged, safety_stock, quality_control.” It also rules the commitment column out of bounds for integrations: “You can’t use the Admin API to adjust or move inventory quantities in the committed state. Inventory quantities in the committed state are only affected by the creation and fulfillment of a merchant’s orders.”
Two consequences follow from that formula. First, placing an order moves units from available into committed and leaves their sum unchanged — so on_hand is invariant under order placement in a way available is not, which makes it structurally less exposed to the race in the previous section. Second, Shopify subtracts only its own committed quantity when it derives available from on_hand, so a value written to on_hand is only correct if Shopify is the only channel committing that stock in NetSuite.
Oracle’s own guidance points the other way for the general case. Its NetSuite Connector documentation states that when syncing from NetSuite locations, the connector “syncs the available quantity — not the quantity on hand”, because “quantity on hand covers everything in the warehouse, even what’s committed to orders” while available quantity “means the amount that’s not committed — so that’s what you can sell”. NetSuite’s quantityAvailable is defined as “the number of units in stock that have not been committed to fulfil sales”, and it nets out commitments from every channel, not just Shopify.
| Write target | Source field in NetSuite | Exposed to the order race? | Correct when other channels commit the same stock? |
|---|---|---|---|
available |
quantityAvailable at the mapped location |
Yes — the value being overwritten changes on every order | Yes — NetSuite has already netted out all channels |
on_hand |
quantityOnHand at the mapped location |
No — order placement leaves the sum unchanged | No — Shopify subtracts only its own committed quantity |
committed |
not applicable | n/a | Not writable through the Admin API |
Verdict: a single-channel Shopify store can write on_hand and get race immunity for free. Any store where wholesale, marketplace, retail or another storefront also commits NetSuite stock must write available, and must therefore solve the race explicitly — which is the next section.
compareQuantity turns a silent oversell into a failed mutation
Shopify ships the fix for the lost update in the mutation itself. inventorySetQuantities performs a compare-and-set: by default it applies the write only if the quantity currently stored matches the compareQuantity you supply, and returns an error without changing anything when they differ. Shopify’s own warning on the alternative is explicit — opting out of the check “can lead to inaccurate inventory quantities if multiple requests are made concurrently”.
The practical shift is that the connector must read Shopify before it writes to Shopify. Instead of pushing a NetSuite number into the void, it reads the current available value, passes it as compareQuantity, and lets Shopify reject the write if a buyer moved the number in between. A rejection is not an error to retry blindly: it is a signal that the state changed, so the correct handling is to re-read both sides and recompute, not to resend with ignoreCompareQuantity set.
The mutation below is the shape to ship, on API version 2026-07, including the idempotency directive covered in the next section.
mutation SetAvailable($input: InventorySetQuantitiesInput!, $key: String!) {
inventorySetQuantities(input: $input) @idempotent(key: $key) {
inventoryAdjustmentGroup {
reason
referenceDocumentUri
changes { name delta quantityAfterChange }
}
userErrors { code field message }
}
}
{
"key": "550e8400-e29b-41d4-a716-446655440000",
"input": {
"name": "available",
"reason": "correction",
"referenceDocumentUri": "netsuite://itemlocationquantity/48211/12",
"ignoreCompareQuantity": false,
"quantities": [
{
"inventoryItemId": "gid://shopify/InventoryItem/30322695",
"locationId": "gid://shopify/Location/124656943",
"quantity": 7,
"compareQuantity": 5
}
]
}
}
Set referenceDocumentUri to something that identifies the NetSuite record and location that produced the number. It is the only field in the payload that will tell an operator six weeks later which system wrote a quantity, and Shopify surfaces it on the resulting adjustment group.
From version 2026-04 the idempotent directive is mandatory
Shopify made the @idempotent directive mandatory in API version 2026-04, announced in its developer changelog on 12 December 2025. The requirement covers 17 mutations, including every inventory write a NetSuite connector uses: inventorySetQuantities, inventoryAdjustQuantities, inventoryMoveQuantities, inventorySetOnHandQuantities, inventoryActivate, the inventory transfer and shipment mutations, locationActivate, locationDeactivate, and refundCreate.
The failure mode is unusual and worth stating plainly: the directive is not mandatory at the schema level, so a call that omits it validates and then fails at runtime. Static schema checks and code generation will not catch it. Shopify documents IDEMPOTENCY_CONCURRENT_REQUEST and IDEMPOTENCY_KEY_PARAMETER_MISMATCH among the error codes this surface returns.
This is a version-pin problem before it is a code problem. A connector pinned to a version older than 2026-04 keeps working exactly as it did, and breaks on the day someone advances the pin — which, given Shopify’s minimum twelve-month support window per stable version, is a day that arrives on a schedule rather than a choice. Request an unsupported version and Shopify falls forward to the oldest supported stable version silently, so an expired pin does not announce itself either.
The directive also changes the cost calculus between the two architectures in this post’s title. A webhook fast-path retries far more than a scheduled job does, both from Shopify’s own delivery retries and from the queue in front of your handler. Without an idempotency key, a retried adjustment applies twice. Real-time raises the stakes on idempotency in a way a five-minute cron does not — a genuine, documented cost of the faster architecture.
Round-trip time, not poll interval, sets the incoherence window
The window during which Shopify and NetSuite disagree is not the poll interval. It is the sum of two legs: the time for a Shopify order to reach NetSuite and commit stock there, plus the time for the next inventory read-and-write to carry the result back to Shopify.
Run the arithmetic on a common configuration. Orders sync into NetSuite on a four-minute cycle; inventory syncs out on a five-minute cycle. The worst-case window is roughly nine minutes, not five. Halving the inventory interval to two and a half minutes takes the total to about six and a half — a 28% improvement in exchange for double the API budget, because the leg that dominates was never touched. Halving the order leg instead does more for the same money.
This is why “sync every N minutes” is an incomplete specification and why the answer to “is five minutes fast enough?” is unanswerable without the other leg. Measure both legs before changing either. The measurement is cheap: stamp the Shopify order creation time, the NetSuite sales order creation time, and the timestamp on the resulting inventory write, then take the distribution rather than the average — the tail is what oversells.
Two design consequences follow. Committing stock in NetSuite at order creation rather than at fulfilment collapses the first leg to the order-sync latency instead of the warehouse’s working rhythm. And a webhook fast-path on the order leg is usually worth more than a shorter poll on the inventory leg, which inverts where most teams spend the effort. The trade-offs on each mechanism are covered in our comparison of webhooks versus polling for inventory sync.
What actually limits how often you can sync
Two documented ceilings bound sync frequency, and neither is a general cost multiple. On the Shopify side the constraint is a points budget; on the NetSuite side it is a concurrency pool.
Shopify’s GraphQL Admin API meters an app at 100 points per second on the Standard plan, 200 on Advanced, 1,000 on Plus and 2,000 on Commerce Components, and caps any single query at 1,000 points regardless of plan. Cost is calculated from what the query returns rather than counted per request, and a mutation costs 10 points by default. The operative consequence is that batching many SKUs into one mutation is dramatically cheaper than one mutation per SKU, so the achievable interval depends far more on how the writes are shaped than on how many items changed.
On the NetSuite side, inbound web services and RESTlet requests share a single account-wide concurrency pool: 5 concurrent requests on Standard, 15 on Premium, 20 on Enterprise and Ultimate, plus 10 per SuiteCloud Plus licence. That pool is shared with every other integration in the account, which is why a connector that opens one connection per SKU exhausts a Standard-tier account at six parallel items. Read the live number at Setup > Integration > Integration Governance rather than assuming the tier, and pair it with the per-script governance budgets — 5,000 units for a RESTlet, 10,000 for a scheduled script — that cap how much work one execution can do. The error handling this demands is specified in our guide to NetSuite concurrency limits and the rejection codes they return.
What does not appear in any vendor documentation is a fixed cost ratio between real-time and scheduled architectures. The honest statement of the difference is structural, not numeric: a scheduled job is one component, while a webhook fast-path adds a public HTTPS endpoint, HMAC verification, a deduplication store, a queue, and a reconciliation job that must exist anyway. Five components, five failure surfaces, and an on-call story. That is the cost to weigh — quantified against your own infrastructure, not against a borrowed multiple.
When a webhook fast-path is justified
A webhook fast-path is justified when the cost of one oversell exceeds the cost of operating five extra components, and only for the flows where it actually helps. It is never justified as a replacement for reconciliation.
Shopify is explicit that webhook delivery is not guaranteed and that ordering is not guaranteed within or across topics. Delivery allows 1 second to connect and 5 seconds for the full request, retries 8 times over 4 hours, and after 8 consecutive failures deletes an API-created subscription silently. An integration that treats inventory_levels/update as a reliable stream will lose events during any outage longer than four hours and will not be told. Real-time architectures are a fast path plus a reconciliation sweep, or they are broken.
The direction matters too. NetSuite has no outbound webhook, so the NetSuite-to-Shopify leg cannot be event-driven from NetSuite’s side by subscription. Pushing on change means a SuiteScript user event deployed to the transaction records that move stock — sales order, item fulfilment, item receipt, inventory adjustment, inventory transfer. Deploying that script to the inventory item record instead is a common and silent mistake: an inventory-affecting transaction does not submit the item record, which is precisely why Last Modified Date does not move, so a user event on the item never fires for a stock movement.
| Signal | Scheduled only | Scheduled plus fast path | Fast path primary |
|---|---|---|---|
| Normal-velocity catalogue | Default choice | Unnecessary complexity | Unjustifiable |
| Limited-quantity drops, promotions | Oversell exposure concentrated | Correct shape | Only with reconciliation |
| Order-triggered warehouse allocation | Adds latency to fulfilment | Correct shape | Only with reconciliation |
| Stock shared with other channels | Workable with compare-and-set | Correct shape | Reconciliation is mandatory |
| Catalogue and price updates | Always sufficient | No benefit | No benefit |
Verdict: scheduled sync with a webhook fast-path on the order leg is the default recommendation for a store with real velocity, and a pure scheduled sync remains correct for most catalogues. Neither is safe without compare-and-set writes.
The decision framework
Work down this list in order. The first five items decide correctness and must be settled before the cadence question is worth asking; the rest size the interval.
- Confirm which NetSuite field the incremental query filters on, and change it to Last Quantity Available Change if it is Last Modified Date.
- Add a monitor that alerts when the sync touches zero items in a window that contained inventory-affecting transactions.
- Decide whether Shopify is the only channel committing this stock; write
on_handif it is,availableif it is not. - Read the current Shopify quantity immediately before every write and pass it as
compareQuantity; never shipignoreCompareQuantity: trueas the default path. - Handle a compare mismatch by re-reading both systems and recomputing, not by resending the same value.
- Add the
@idempotentdirective with a stable key to every inventory mutation before advancing the pinned API version to 2026-04 or later. - Record the pinned Shopify API version in configuration and set a calendar check against the quarterly release notes.
- Measure both legs of the round trip separately and record the tail of each distribution, not the average.
- Batch inventory writes into as few mutations as the 1,000-point single-query cap allows.
- Read the account’s real concurrency limit at Setup > Integration > Integration Governance before sizing parallelism.
- Deploy any push-on-change SuiteScript to the transaction records that move stock, never to the inventory item record.
- Run a reconciliation sweep on a fixed schedule regardless of architecture, and alert on drift rather than assuming the fast path caught everything.
Get the working checklists
The runbooks and decision checklists from these guides, as printable PDFs — free in the SoftXone guide library.
Where this framework does not apply
The write-path rules above govern inventory quantities, which are numeric, contended, and overwritten in place. They do not transfer to every flow in the integration. Order creation is contended but not overwritten — the correct control there is an external ID that makes a duplicate create fail on uniqueness, which is a different mechanism from compare-and-set. Catalogue and price updates are rarely contended at all, so a scheduled push without compare semantics is appropriate and the cadence question genuinely reduces to freshness against cost.
The framework also assumes a single mapped location per Shopify location. Multi-location and multi-subsidiary configurations add a mapping question this post does not answer: which NetSuite locations aggregate into which Shopify location, and whether stock a store cannot ship from should be visible at all. Aggregating across locations makes every quantity in this post a sum, and a sum is only as correct as its least-current term. Teams running that shape should settle the mapping before tuning any interval, and the failure patterns it produces are collected in our write-up of the sync problems that surface in month two.
Finally, none of this makes a judgement about whether to build or buy the connector. The mechanisms here are properties of the two APIs and apply identically to a custom integration and to a packaged one — the difference is only whether you can inspect the write path. If you cannot determine from a product’s documentation whether it passes compareQuantity, that is a question worth asking before the contract, and it is the kind of thing our NetSuite integration work for Shopify stores specifies up front.
References
- Inventory ItemsOracle NetSuite Help — states that Last Modified Date updates only on item record edits while Last Quantity Available Change tracks inventory-affecting transactions.
- getItemAvailabilityOracle NetSuite Help — field definitions for quantity on hand, available and committed, the lastQtyAvailableChange filter, and the 10,000-record ceiling.
- Troubleshooting Inventory Sync Issues in NetSuite ConnectorOracle NetSuite Help — Oracle’s statement that available quantity, not quantity on hand, is what syncs to a sales channel.
- Syncing Quantity UpdatesOracle NetSuite Help — quantity sync options for single-location, per-warehouse and aggregated configurations.
- Web Services and RESTlet Concurrency GovernanceOracle NetSuite Help — the account-wide concurrency pool shared by all inbound integrations.
- Manage inventory quantities and statesShopify Developers — the eight inventory states, the on_hand sum formula, and the rule that committed cannot be adjusted through the Admin API.
- inventorySetQuantitiesShopify Developers — compare-and-set semantics, the ignoreCompareQuantity warning, and the current input schema.
- Making idempotency mandatory for inventory adjustments and refund mutationsShopify developer changelog, 12 December 2025 — the 17 affected mutations and the 2026-04 version cutover.
- API rate limitsShopify Developers — calculated query cost, per-plan points-per-second budgets, and the 1,000-point single-query cap.
- API versioningShopify Developers — quarterly release cadence, minimum support window, and fall-forward behaviour for unsupported versions.
Frequently asked questions
Does syncing more often reduce overselling?
Only marginally, and only once the write path is already correct. A shorter interval reduces the quantity that can be committed between a read and a write, but an absolute write with no compare-and-set check still returns committed units to sale on every cycle. Correcting the write semantics removes the failure class outright; shortening the interval only reduces how much stock each occurrence exposes.
Which NetSuite field should an incremental inventory query filter on?
Last Quantity Available Change. Oracle documents that Last Modified Date moves only when the item record itself is edited, so a query filtered on it returns no rows after a fulfilment or an inventory adjustment even though stock moved. The SuiteTalk getItemAvailability operation exposes the same value as a lastQtyAvailableChange request filter, capped at 10,000 items per call.
What happens if the idempotent directive is omitted on API version 2026-04?
The call fails at runtime rather than at schema validation, so code generation and static checks will not catch it before deployment. Shopify documents IDEMPOTENCY_CONCURRENT_REQUEST and IDEMPOTENCY_KEY_PARAMETER_MISMATCH among the errors this surface returns. Because the directive is not marked required in the schema, add keys to every inventory mutation before advancing the pinned version rather than after.
Can a NetSuite user event script push inventory changes to Shopify?
Yes, but only when it is deployed to the transaction records that move stock: sales order, item fulfilment, item receipt, inventory adjustment and inventory transfer. Deploying it to the inventory item record does nothing for stock movements, because an inventory-affecting transaction does not submit the item record. NetSuite offers no outbound webhook subscription, so a script on those transactions or a scheduled query is the only push mechanism.
Should a safety-stock buffer be held in NetSuite or in Shopify?
Shopify exposes a safety_stock state that counts toward the on_hand sum and is excluded from available, so the buffer can be held on the Shopify side without altering the quantity NetSuite reports. Keeping it there leaves NetSuite true to the warehouse and makes the buffer visible to whoever investigates an oversell, instead of hiding it inside connector configuration where it is easily forgotten.

Leave a Reply