WooCommerce and NetSuite
WooCommerce NetSuite integration
WooCommerce orders arrive in NetSuite as sales orders with the right customer, tax and payment. Stock, prices and tracking come back. The integration reads the WooCommerce REST API and your plugins' order data, is sized to your NetSuite concurrency limit, and is hosted and run for you.
What moves between WooCommerce and NetSuite
WooCommerce captures the order and the payment. NetSuite fulfills, invoices and books it. Six flows cover the pair.
| Flow | Direction | WooCommerce | NetSuite |
|---|---|---|---|
| Orders | to NetSuite | GET /wc/v3/orders |
Sales Order or Cash Sale |
| Payments | to NetSuite | payment_method, transaction_id |
Customer Deposit on an account per gateway |
| Customers | to NetSuite | billing, shipping, customer_id |
Customer, company or individual |
| Fulfillment | to WooCommerce | status completed, tracking |
Item Fulfillment with carrier and tracking number |
| Refunds | both ways | /orders/<id>/refunds |
Credit Memo or Cash Refund, with restock |
| Items, prices, stock | to WooCommerce | products and variations | items, matrix items, price levels |
Gateway captures are their own mapping. A Stripe capture and a PayPal capture land on different clearing accounts in NetSuite, so that the payout from each gateway can be reconciled against its own deposits. The gateway comes from payment_method, the reference from transaction_id.
Which system owns what
| Data | System of record | Direction | Sync behavior |
|---|---|---|---|
| Items, SKUs, matrix structure | NetSuite | NetSuite to WooCommerce | changed items, on a schedule |
| Product pages, images, categories | WooCommerce | stays in WooCommerce | not touched |
| Regular and sale price | NetSuite price level | NetSuite to WooCommerce | regular_price, sale_price |
| Stock quantity | NetSuite, selected locations | NetSuite to WooCommerce | one sum per product or variation |
| Orders | WooCommerce until import | WooCommerce to NetSuite | webhook plus poll |
| Customers | WooCommerce for web customers | WooCommerce to NetSuite | matched on billing email |
| Order status | NetSuite after import | NetSuite to WooCommerce | processing to completed |
| Refunds | where the refund starts | both ways | one refund per credit memo |
WooCommerce core has no price per customer and no company account: company is a text field inside the billing address. B2B prices in a WooCommerce store come from an extension. Where one is installed, NetSuite price levels are written to that extension's fields. See customer-specific pricing.
What WooCommerce and NetSuite store differently
Guest orders and customers
A guest order has customer_id 0. NetSuite accepts no sales order without a customer. The integration matches on billing email first, creates a customer when there is no match, and uses the company field to decide between a company and an individual. Placing every web customer under one parent customer works until the hierarchy reaches NetSuite's limit of 10,000 customers. The matching chain is described in duplicate customers and orders.
The order itself
{
"id": 100482,
"status": "processing",
"customer_id": 0,
"prices_include_tax": false,
"payment_method": "stripe",
"billing": {
"company": "Bakker Installatie B.V.",
"address_1": "Industrieweg 12",
"postcode": "3542 AD",
"country": "NL"
},
"line_items": [
{ "sku": "TS-BLK-M", "quantity": 3, "subtotal": "33.67", "total": "30.30" }
],
"coupon_lines": [ { "code": "fall10", "discount": "3.37" } ],
"fee_lines": [ { "name": "Small order fee", "total": "2.50" } ],
"meta_data": [ { "key": "_po_number", "value": "PO-7781" } ]
}
Four parts of this order have no direct place in NetSuite. coupon_lines needs a discount item. fee_lines needs a non-inventory or other charge item per kind of fee. shipping_lines becomes a ship method and a shipping cost. meta_data holds whatever your plugins added: a PO number, a delivery date, a VAT number. Each key that matters maps to a custom body field such as custbody_po_number or to a custom segment.
Tax and rounding
WooCommerce stores tax per line and per rate, and a store setting decides whether tax is rounded per line or at subtotal level. Three lines of 10.10 at 8.25 percent give 0.83 each when rounded per line: 2.49. Rounded on the subtotal of 30.30 the tax is 2.50. NetSuite recalculates on the sales order unless the integration writes the store's amounts. One cent of difference keeps a deposit from matching its invoice automatically. prices_include_tax adds a second switch: a store that enters prices including tax sends gross prices, and NetSuite expects net. The integration takes WooCommerce's line totals and tax amounts as given and writes them to NetSuite, so both sides show the amount the customer paid. More in money, tax and rounding.
Variable products and matrix items
A variable product has type variable, and its variations are at /products/<id>/variations, each with its own id, sku, regular_price and stock_quantity. A NetSuite matrix has a parent and child items, with at most 2,000 combinations. Order lines carry both product_id and variation_id; the integration maps the variation to the matrix child by SKU and keeps both IDs in an item map. A variation can be saved without a SKU. Those are reported before go-live, since an order line without a SKU cannot be resolved to an item.
One stock number
A product or variation has one stock_quantity, a stock_status of instock, outofstock or onbackorder, and a backorders setting. There is no location. NetSuite has a quantity per location. The integration sums the locations that may fulfill web orders and writes one number. See multi-location inventory.
The technical reality of this pair
| Limit | WooCommerce | NetSuite |
|---|---|---|
| Throttle | none documented for /wc/v3; the maximum is what the host can handle |
5, 15 or 20 concurrent requests |
| Page size | 10 by default, 100 at most | 1,000 rows per page |
| Batch | 100 objects per request, HTTP 413 above | 5,000 usage units per RESTlet call |
| Webhooks | sent once; switched off after 5 consecutive failures | none; polled or pushed by script |
| Incremental read | modified_after, since WooCommerce 5.8 |
last modified date in SuiteQL |
| Authentication | consumer key and secret of one WordPress user | OAuth 2.0, token valid 60 minutes |
Stock, worked through. A store with 1,500 variable products and 12,000 variations looks like 120 batch requests of 100. It is not: the variation batch endpoint is per parent product, /products/<product_id>/variations/batch. A full stock update is 1,500 requests of 8 variations on average. Every request starts WordPress and its plugins on your host. So the integration sends only variations whose quantity changed, and spaces its requests so that checkout keeps its share of the PHP workers. On the NetSuite side the same 12,000 quantities are 12 SuiteQL pages.
Webhooks and polling.
A WooCommerce webhook is signed with X-WC-Webhook-Signature, a base64 HMAC-SHA256 of the payload, and delivered from WordPress's background queue. Any response other than 2xx, 301 or 302 counts as a failure. After five consecutive failures WooCommerce disables the webhook, and its documentation describes no retry. An integration that depends on webhooks alone loses orders the first time its endpoint is unreachable, and stops receiving them altogether a few orders later. Ours accepts the webhook into a queue and also polls GET /wc/v3/orders?modified_after=... with per_page=100. It checks that the webhook is still active and switches it back on. Background in webhooks are not a guarantee.
Orders into NetSuite. NetSuite is the slower side. At 3 seconds per sales order and 2 of the 5 slots of a Standard account, the maximum is 40 orders a minute. External ID WOO-100482 on the sales order makes a retry an update. The numbers are on the NetSuite page and in NetSuite concurrency limits.
Versions. WooCommerce removed its legacy REST API from core in version 9.0. An integration written against /wc-api/v3 stopped working there unless the separate legacy plugin was installed. The current API is /wp-json/wc/v3.
Where standard connectors stop
A standard connector maps the documented order schema: lines, addresses, totals, status. In a WooCommerce store a large part of the order is stored outside that schema, and it differs per store.
- Plugin data in
meta_data. Subscriptions, deposits, delivery dates, B2B roles and VAT numbers are stored under keys chosen by each plugin's author. No fixed mapping knows them. - Tracking. The documented order has no tracking field. The Shipment Tracking extension has its own endpoint,
/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings, and no call to update a tracking item once it exists. - Refunds that pay out.
api_refundis true by default. A connector that mirrors a NetSuite credit memo into a WooCommerce refund without setting it to false triggers the payment gateway. - Stock per location. With one stock number in WooCommerce, the choice of which NetSuite locations count is a rule that has to be written down somewhere. A fixed setting for one location does not cover a second warehouse.
A built integration starts from an inventory of your plugins and the keys they write. The technical comparison is in custom integration vs iPaaS.
When it goes wrong
| What fails | What the integration does | What you see |
|---|---|---|
| Webhook delivery fails | next poll picks up the order | a log line: found by poll |
| Webhook disabled by WooCommerce | re-enables it, keeps polling | an alert with the time it was disabled |
| API key returns 401 | stops writing, keeps the queue | an alert: key or user removed |
| REST API returns 404 on every call | stops, queue intact | an alert: permalinks set to Plain |
| NetSuite answers 429 | retries with a growing pause | nothing, unless it lasts |
| Order line without a known SKU | holds that order | the order and the line |
An order that still fails after its retries is kept with its payload and can be sent again after the cause is fixed. A WordPress update, a plugin update or a host migration is the usual moment for one of the first four rows; the alert arrives before a customer asks where the order is.
Before go-live the integration runs as a dry run: it reads your live orders and shows the sales orders, customers and deposits it would create in NetSuite, without writing. From then on each run leaves a diff per record in the dashboard. We host and monitor it as part of an e-commerce ERP integration. The WooCommerce page covers the platform side. Sibling pages: Shopify NetSuite integration, Magento NetSuite integration and BigCommerce NetSuite integration. Tell us which plugins your store runs.
Frequently asked questions
Does WooCommerce need a plugin for the NetSuite integration?
No. The integration runs outside WordPress and talks to the WooCommerce REST API with a consumer key and secret. On the WordPress side it needs HTTPS, permalinks set to anything other than Plain, and a dedicated user for the API key. A key stops working when its WordPress user is deleted, so it should not belong to a personal account.
How fast do WooCommerce orders reach NetSuite?
With the order.created webhook, within seconds. WooCommerce sends each webhook once and switches a webhook off after five consecutive failed deliveries, so the integration also polls for orders changed since the last run. An order the webhook missed arrives with the next poll.
Can WooCommerce show stock from several NetSuite locations?
WooCommerce core holds one stock quantity per product or variation and has no location field. The integration adds up the NetSuite locations you select, subtracts a buffer if you set one, and writes the result. With 14 at Main, 6 at Store and a buffer of 2, WooCommerce gets 18.
How are WooCommerce refunds booked in NetSuite?
A refund in WooCommerce becomes a Credit Memo or Cash Refund in NetSuite, with or without a restock, depending on the refund's line items. In the other direction a NetSuite credit memo creates a WooCommerce refund with api_refund set to false when the money has already been returned, so the payment gateway does not refund it a second time.
How does the tracking number get back into WooCommerce?
The documented WooCommerce order has no tracking field. With the Shipment Tracking extension the integration posts tracking_number, tracking_provider and date_shipped to the extension's own endpoint. Without it, the tracking number goes into an order note and order meta data, and the order moves to completed.
Does the integration work with High-Performance Order Storage?
Yes. HPOS has been the default for new stores since WooCommerce 8.2 and moves orders from the posts tables to four tables of their own. The integration reads and writes orders through the REST API only, never through the database, so the storage mode makes no difference to it.
Tell us what you need
Tell us which systems you use and what needs to work better. We reply personally within one working day. We help you define the work, then agree the price and timing before you commit.
- You know the price and the timeline before you commit to anything
- We work out the connection that takes the work off your hands
- We handle the build, hosting, monitoring and maintenance
- Your team has less information to retype and fewer discrepancies to chase