Webhooks vs Polling for NetSuite-to-Shopify/WooCommerce Inventory Sync
- Polling on a 5-15 minute schedule is correct for most catalogs — NetSuite has no native inventory-change webhook, so any webhook path is a SuiteScript User Event you build and govern yourself.
- A User Event script has a 1,000-unit governance ceiling and
https.post()costs 10 units each call — do that arithmetic before assuming a per-record webhook survives a bulk change. - NetSuite’s own CSV import screen has a “Run Server SuiteScript and Trigger Workflows” checkbox that Oracle’s own guidance says to disable for large imports — exactly the bulk inventory updates most likely to need the webhook you built.
- Shopify’s REST Admin API, still shown in most integration walkthroughs, has been legacy since October 2024; new inventory writes belong on the GraphQL
inventorySetQuantitiesmutation, which now requires an idempotency key.
The inventory sync frequency debate comes down to one question: how stale can your storefront stock be before it causes a real problem? For most merchants, a 5-minute lag between NetSuite adjusting inventory and the store reflecting that change is acceptable. For a store running a flash sale on 50 units, it is not. What most guides on this topic skip is what happens after you pick an architecture: a webhook path has a governance budget that a bulk change can exhaust, NetSuite’s own CSV import screen has a setting that can silently turn that webhook off, and the Shopify endpoint most walkthroughs still show is on its way out. This covers both approaches and the platform-specific failure modes attached to each.
Contents
- Polling: The Simpler Default
- Batch the Writes: WooCommerce’s 100-Item Ceiling
- Webhooks: When You Need Sub-Minute Updates
- The Governance Ceiling on a Per-Record Webhook
- The Import Setting That Silently Turns Your Webhook Off
- Shopify’s Write Target Moved: REST to GraphQL
- Comparison: When to Use Each
- The Hybrid Architecture
Polling: The Simpler Default
A scheduled job queries NetSuite for inventory changes since the last run and pushes them to the store. No webhook infrastructure needed on either side — no public endpoint to secure, no retry queue to build, no secret to rotate. The integration layer is always the party initiating the connection, which is also why polling survives an outage that a webhook path does not: the next scheduled run picks up everything that changed while the integration was down, with no dead-letter handling required.
Average order-to-next-sync lag is acceptable (5–15 minutes). Catalog is stable — less than 20% of SKUs change inventory in any given sync window. You do not run flash sales or time-limited drops where simultaneous demand exceeds available stock.
Batch the Writes: WooCommerce’s 100-Item Ceiling
A polling script that updates one SKU per REST call is the most common performance mistake in a WooCommerce sync, and it is an easy one to avoid: WooCommerce’s REST API ships a dedicated batch endpoint at /wc/v3/products/batch that accepts create, update, and delete arrays in a single POST. A five-minute poll that finds 40 changed SKUs should send one batch request, not 40 sequential ones.
The ceiling on that batch is lower than it looks. WooCommerce’s own REST controller counts create, update, and delete items together against a single limit — a request with 60 updates and 50 creates is 110 items against a 100-item cap, not two separate requests worth 60 and 50. The default limit is 100, set by the woocommerce_rest_batch_items_limit filter, and exceeding it does not partially apply: the endpoint rejects the whole request with an HTTP 413 and no items are written. A sync job that groups changed SKUs into fixed batches of 100 or fewer, split by operation type only where the combined total requires it, avoids the rejection entirely.
Webhooks: When You Need Sub-Minute Updates
NetSuite does not natively emit an event when inventory changes. You must build this. A SuiteScript User Event script on the Inventory Item record fires on afterSubmit — the trigger Oracle documents for anything that should happen after a record is saved, including syncing with an external system — when the quantityAvailable field changes, then calls your integration endpoint via https.post().
// UserEvent script on InventoryItem (afterSubmit)
define(['N/https', 'N/runtime'], function(https, runtime) {
function afterSubmit(context) {
var newRec = context.newRecord;
var oldRec = context.oldRecord;
var newQty = newRec.getValue('quantityavailable');
var oldQty = oldRec ? oldRec.getValue('quantityavailable') : null;
if (newQty === oldQty) return; // no change
https.post({
url: runtime.getCurrentScript().getParameter('custscript_webhook_url'),
headers: { 'Content-Type': 'application/json',
'X-Webhook-Secret': runtime.getCurrentScript()
.getParameter('custscript_webhook_secret') },
body: JSON.stringify({
item_id: newRec.id,
sku: newRec.getValue('itemid'),
qty_available: newQty
})
});
}
return { afterSubmit: afterSubmit };
});
This is the shape nearly every “how to add NetSuite webhooks” walkthrough shows, and it is correct for a single record change. What it leaves out is what happens when a hundred records change in the same operation — the case that matters most, because a flash-sale restock or a cycle count is exactly when a merchant most wants the webhook path to be reliable.
The Governance Ceiling on a Per-Record Webhook
Every SuiteScript API call costs governance units, and Oracle’s own governance table prices https.post() at 10 units per call. A User Event script has a total budget of 1,000 usage units for its entire execution — not 1,000 per API call, 1,000 total, shared across every operation the script performs, including the field reads that happen before the webhook fires. That is a hard ceiling of roughly 100 https.post() calls even in a script that does nothing else, and meaningfully fewer once record loads and field lookups are counted against the same budget.
A Scheduled Script gets 10,000 usage units — ten times the room. This is the practical argument for routing bulk-change notifications through a Scheduled Script that batches changed records into fewer, larger webhook payloads, rather than firing one https.post() per record from a User Event script triggered on every save. A User Event script that emits one webhook per changed inventory item is fine for the ones-and-twos case a flash sale represents. It is not sized for a supplier shipment that updates 300 SKUs in one operation — that run either exhausts its governance budget and throws, or the platform defers the excess work, and either outcome is a webhook path that silently stopped covering some of the records it was supposed to.
The Import Setting That Silently Turns Your Webhook Off
The most common way a NetSuite inventory count changes in bulk is not a form edit — it is a CSV import, run for a cycle count reconciliation, a supplier receipt, or a warehouse audit. NetSuite’s CSV import screen carries an option called “Run Server SuiteScript and Trigger Workflows.” Left unchecked, it turns off every server-side SuiteScript and workflow trigger for that import job, including the User Event script emitting your webhook — the import completes, the inventory record updates, and nothing calls your endpoint.
The trap is that Oracle’s own guidance points admins toward unchecking it. Running server-side SuiteScript during an import slows the save process, and Oracle recommends disabling script and workflow triggers specifically for large bulk imports where “real-time automation is not needed” — historical data loads being the stated case. A cycle count reconciliation is a large bulk import by definition, and it is exactly the opposite of a case where real-time automation is not needed: it is often the single update most likely to leave a storefront overselling stock that no longer exists. The setting that keeps a big import fast is the same setting that can quietly disable the one mechanism keeping the storefront honest during that import.
Whether the “Run Server SuiteScript and Trigger Workflows” box is checked for that specific import — not just the company-wide default, since it can be overridden per import and saved that way. If a team routinely disables it to speed up large imports, the webhook path is not covering those imports, no matter how correct the SuiteScript is. This is precisely why the polling path matters even when a webhook exists: a scheduled poll picks up whatever the webhook missed, on the next run, with no separate detection required.
Shopify’s Write Target Moved: REST to GraphQL
Shopify’s REST Admin API — the InventoryLevel endpoint most integration walkthroughs, including the earlier version of this one, point NetSuite syncs at — has been legacy since October 1, 2024. Shopify requires new public apps to be built on the GraphQL Admin API as of April 1, 2025. A NetSuite integration built against the REST inventory endpoint today is building against an interface Shopify has already stopped recommending for new work, even though it still functions.
The GraphQL replacement is not a one-to-one rename. Shopify ships two separate mutations for two separate jobs: inventorySetQuantities overwrites inventory to an absolute value and supports compare-and-set — pass the quantity your system currently believes is correct as compareQuantity, and the write only applies if that still matches what Shopify has on file, which is exactly the concurrency protection a NetSuite-is-the-source-of-truth sync needs. inventoryAdjustQuantities instead applies a relative delta. Shopify’s own documentation is explicit about which one fits an ERP-driven sync: inventorySetQuantities is for “a system that acts as the source of truth for inventory quantities” — a NetSuite-to-Shopify polling or webhook script is precisely that case.
One more requirement changed recently enough that older integration code will fail on it: as of the 2026-04 API version, inventorySetQuantities requires an idempotency key, supplied through the @idempotent directive. It was optional in 2026-01 and earlier. A sync script written against an earlier API version and never revisited will start rejecting writes the moment Shopify sunsets the version it was pinned to — worth checking now, not at the next forced API version bump.
Comparison: When to Use Each
| Factor | Polling | Webhooks |
|---|---|---|
| Latency | 5–15 minutes typical | Seconds |
| Infrastructure complexity | Low — scheduled job and a batch-aware write only | High — endpoint, secret validation, retry queue |
| NetSuite setup required | SuiteQL query only | Custom User Event script, governed at 1,000 units per run |
| Bulk-change reliability | Unaffected — next poll always re-reads current state | At risk from governance ceilings and the CSV import trigger setting |
| Failure recovery | Natural — next poll catches missed changes | Must implement retry queue and dead-letter handling |
| Best for | Standard catalogs, steady-state inventory | Flash sales, limited drops, high-velocity SKUs |
The Hybrid Architecture
The hybrid approach is correct for most growing merchants: poll every 5 minutes for the full catalog, using a SuiteQL query against a genuinely last-modified timestamp, and batch every write against the target platform’s own ceiling — 100 combined items per WooCommerce REST batch, a properly scoped GraphQL mutation on Shopify. Layer a webhook on top only for a defined “high-velocity” SKU set — items that have sold out within 24 hours in any 30-day window — and route that webhook through a Scheduled Script rather than a User Event script once the changed-item count in a single run is likely to exceed a handful, so the 10,000-unit budget applies instead of the User Event script’s 1,000.
This keeps the architecture simple for the 80–95% of a catalog that does not need sub-minute accuracy, while giving the SKUs that do need it a path that is not silently disabled by an import setting or exhausted by a bulk change. Whether webhooks, polling, or both is the right call for a given catalog is really the same question our real-time vs. scheduled sync decision framework works through from the oversell-risk side rather than the transport side, and the interface each approach rides on — REST, RESTlet, or SuiteQL — is covered in full in our comparison of NetSuite’s REST API and SuiteScript RESTlet.
If you are weighing this for a live catalog rather than a greenfield build, NetSuite Integration Pro ships the batched, resumable polling loop for WooCommerce described above, and NetSuite Integration for Shopify covers the GraphQL-side equivalent, including the idempotency-key handling above.
Get the working checklists
The runbooks and decision checklists from these guides, as printable PDFs — free in the SoftXone guide library.
References
- SuiteScript afterSubmit(context)Oracle NetSuite Help — the User Event trigger used to detect inventory changes and emit a webhook.
- SuiteScript 2.1 API GovernanceOracle NetSuite — the 10-unit cost of https.post() and the 1,000 / 10,000 usage-unit ceilings for User Event and Scheduled scripts.
- CSV Import: Server Scripting and Workflow ExecutionOracle NetSuite — the “Run Server SuiteScript and Trigger Workflows” import setting and Oracle’s guidance to disable it for large imports.
- inventorySetQuantities mutationShopify GraphQL Admin API — absolute-value inventory writes, compare-and-set, and the 2026-04 mandatory idempotency key.
- WooCommerce REST API DocumentationWooCommerce — the products batch endpoint used to write changed SKUs in a single request.
- WooCommerce REST API — batch limit sourceWooCommerce REST API GitHub repository — the check_batch_limit() method: the 100-item combined default and the HTTP 413 rejection behavior.
Frequently asked questions
When is polling good enough?
For most inventory sync. It is simpler, self-healing after outages, and predictable in the load it places on both systems.
When do I actually need webhooks?
When sub-minute accuracy genuinely matters: high-velocity SKUs, limited stock, or channels that punish overselling. Size the webhook path against NetSuite’s 1,000-unit User Event budget before promising sub-minute accuracy across a large catalog.
Can I use both together?
Yes, and it is often the right answer. Poll on a schedule as the baseline, and add event-driven updates only for the SKUs where seconds matter, routed through a Scheduled Script once volume outgrows a single User Event script’s budget.
Will a bulk NetSuite CSV import always trigger my webhook script?
Not necessarily. The import screen’s “Run Server SuiteScript and Trigger Workflows” box has to be checked for that specific import, and Oracle’s own guidance recommends unchecking it for large imports to speed up the save, which is exactly the kind of bulk inventory change most likely to need the webhook. Confirm the setting per import rather than assuming the company default covers it.
Should a new NetSuite-to-Shopify sync use the REST or GraphQL API for inventory?
GraphQL. Shopify’s REST Admin API has been legacy since October 2024 and closed to new public apps since April 2025. Use the inventorySetQuantities mutation with compareQuantity for concurrency-safe absolute writes, and note that an idempotency key became mandatory in the 2026-04 API version.

Leave a Reply