WooCommerce and your ERP
WooCommerce ERP integration
WooCommerce publishes no API rate limit: the limit is your web host, and every API call starts WordPress and its plugins. Orders carry plugin data under keys nobody documented, and stock is one whole number per product. This page gives the numbers for WooCommerce and six ERPs. We build the integration to your ERP and your plugins, then host and run it.
What moves between WooCommerce and an ERP
WooCommerce takes the order and the payment. The ERP holds items, prices, stock, customers on account and the books. Each kind of data has one owner, and the integration writes in one direction per field.
| Data | System of record | Direction | WooCommerce side |
|---|---|---|---|
| Items, SKUs, variations | ERP | ERP to WooCommerce | /products, /products/<id>/variations |
| Descriptions, images, categories | WooCommerce | stays in WooCommerce | not touched by the ERP |
| Regular and sale price | ERP | ERP to WooCommerce | regular_price, sale_price |
| Stock | ERP or WMS | ERP to WooCommerce | stock_quantity, one integer |
| Orders | WooCommerce until import | WooCommerce to ERP | /orders, webhook plus poll |
| Customers | ERP for accounts, WooCommerce for web buyers | both ways | billing, shipping, customer_id |
| Order status and tracking | ERP or WMS | ERP to WooCommerce | status, order notes or a tracking extension |
| Refunds | where the refund starts | both ways | /orders/<id>/refunds |
Three things in this table are not in WooCommerce core and come from plugins when a store has them: a price per customer, a company account and a tracking number. Where they live differs per store, so the integration starts with an inventory of the active plugins.
What WooCommerce's API allows
No published limit, a real one
The REST API at /wp-json/wc/v3 documents no rate limit. WooCommerce's built-in rate limiting covers only POST requests on the Store API, the API for the storefront, and is off by default. The working limit is the host: every API call boots WordPress, loads every active plugin and holds one PHP worker until it returns. A shop on a small plan with four workers that receives a burst of API calls has fewer workers for its checkout.
| Setting | Value |
|---|---|
| Page size | 10 by default, per_page up to 100 |
| Batch | 100 objects per request, HTTP 413 above |
| Variation batch | per parent product: /products/<id>/variations/batch |
| Incremental read | modified_after, since WooCommerce 5.8 |
| Authentication | consumer key and secret of one WordPress user |
| Stock | stock_quantity, integer; manage_stock can be parent |
Catalog, worked through
A store with 800 simple products and 400 variable products with 6,000 variations. The simple products are 8 batch requests of 100. The variations cannot be batched across products: they are 400 requests, one per parent, of 15 variations on average. At an assumed 2 seconds per batch call on a mid-size host, a full update is 816 seconds, a little under 14 minutes of continuous load. A normal run sends only what changed: 300 changed variations spread over 120 products is 120 requests.
Webhooks
WooCommerce delivers webhooks in the background through WP-Cron, and WP-Cron runs only when someone loads a page. On a quiet night an order webhook waits for the next visitor. Any answer other than 2xx, 301 or 302 counts as a failure; the documentation describes no retry, and after 5 consecutive failures the webhook status becomes disabled. So the webhook starts the import and a poll picks up what it missed: GET /wc/v3/orders?modified_after=...&per_page=100 every few minutes picks up whatever the webhook did not deliver. Background in webhooks are not a guarantee.
What WooCommerce and an ERP store differently
Dates
date_created is in the site's time zone and carries no offset. date_created_gmt is the same moment in UTC. A store in New York that takes an order at 8:30 pm on September 30 has a GMT date of October 1, 00:30. An ERP fed the GMT date books a September sale in October. The integration takes the date in the time zone the books are kept in. More in dates and time zones.
Orders and statuses
WooCommerce statuses are pending, processing, on-hold, completed, cancelled, refunded, failed and trash. Stock goes down at processing and on-hold; a pending order is waiting for payment and has not taken stock. The integration creates the ERP sales order at processing, or at on-hold for bank transfer and check payments, and writes completed back when the ERP ships.
A guest order has customer_id 0. The ERP needs a customer, so the integration matches on billing email, creates a customer when there is no match, and uses billing.company to choose between a company and a person. Without a fixed matching rule the same buyer ends up as three ERP customers; see duplicate customers and orders.
Plugin data
Discounts arrive in coupon_lines, surcharges in fee_lines, shipping in shipping_lines. Everything plugins add arrives in meta_data, under keys each plugin author chose: a PO number, a delivery date, a VAT number, a subscription ID.
"meta_data": [
{ "key": "_po_number", "value": "PO-7781" },
{ "key": "_delivery_date", "value": "10/14/2026" },
{ "key": "_billing_vat", "value": "" }
]
Each key that matters is mapped to an ERP field, with its format: 10/14/2026 is a US date string, not a date. When a plugin update renames a key, the field arrives empty; the integration reports an order where a required key is missing instead of writing it without.
Stock
One stock_quantity per product or variation, an integer, no location. An ERP that keeps stock per warehouse, or in decimals for goods sold by the foot or the pound, is summed and rounded down before it is written. A variation with manage_stock set to parent has no stock of its own and sells from the parent's quantity, while the ERP holds stock per SKU. Those variations are found before go-live and either switched to their own stock or written as one sum. See multi-location inventory.
Which ERP
| ERP | API | Limit | Over the limit |
|---|---|---|---|
| NetSuite | REST, SuiteQL, RESTlets | 5, 15 or 20 concurrent requests per account | 429 CONCURRENCY_LIMIT_EXCEEDED |
| Business Central | API v2.0, OData v4 | 6,000 requests per 5 minutes and 5 concurrent, per user | 429 |
| SAP Business One | Service Layer, OData | none published; session of 30 minutes, 20 rows per page | 401 on an expired session |
| Odoo | JSON-2 from version 19 | external API on the Custom plan only | not published |
| ERPNext | Frappe REST API | off unless the site sets rate_limit |
429 |
| QuickBooks Online | REST v3 | 500 requests per minute per company | 429, wait 60 seconds |
NetSuite. Concurrency is shared by every integration on the account, and a sales order created and saved in a script costs 30 governance units. Worked out in WooCommerce NetSuite integration.
Business Central. OAuth 2.0 only since October 2022; 429 above 6,000 requests in five minutes per user; an operation longer than 8 minutes is stopped. The standard item API returns one inventory number, the same shape as WooCommerce's single quantity. Per-location stock needs a custom API page. More on the Business Central page.
SAP Business One. The Service Layer logs in with POST /Login and returns a B1SESSION cookie that expires after 30 idle minutes, with 20 rows per page unless odata.maxpagesize is set. SAP publishes no request limit, so the server sizing decides. Webhooks exist since 10.0 FP 2602 and are off by default. More on the SAP Business One page.
Odoo. JSON-2 at /json/2/<model>/<method> since Odoo 19; XML-RPC and JSON-RPC are removed in Odoo 22. An API key of a normal user expires after at most 90 days. More on the Odoo page.
ERPNext. GET /api/resource/Sales%20Order returns 20 records by default and only their names; fields and limit_page_length change that. Authentication is a header Authorization: token api_key:api_secret for a dedicated user. Frappe's rate limiter is off unless the site config sets rate_limit, and when it is on it counts seconds of request time per window, not requests. See Shopify ERPNext integration for the ERPNext data model.
QuickBooks Online. 500 requests per minute per company and 10 per second per company and app. An item holds one QtyOnHand, inventory needs the Plus or Advanced plan, and DisplayName must be unique across customers, vendors and employees. More on the QuickBooks page.
Where standard connectors stop
A standard connector maps the documented order schema. In a WooCommerce store a large part of the order lives outside it, and it differs per store.
meta_data. PO numbers, delivery dates, VAT numbers and subscriptions sit under plugin keys. A fixed mapping does not know them, and a plugin update can rename them.- Prices per customer. Core has none. The plugin that provides them decides where they are stored, so a connector that writes
regular_pricealone cannot carry an ERP price level. - Refunds that pay out.
api_refundis true by default. Mirroring an ERP credit memo into a WooCommerce refund without setting it to false triggers a second refund at the payment gateway. - Polling the whole catalog. On a host without an API limit, a connector that reads every product every 15 minutes is a constant load on the PHP workers.
A built integration starts from your plugins and the keys they write. The comparison on technical grounds is in custom integration vs iPaaS.
When it goes wrong
| What fails | What the integration does | What you see |
|---|---|---|
| Webhook delivery fails | next poll with modified_after picks up the order |
a log line: found by poll |
Webhook set to disabled |
sets it back to active, keeps polling |
an alert with the time |
| API key returns 401 | stops writing, keeps the queue | an alert: key or user removed |
| Every call returns 404 | stops, queue intact | an alert: permalinks set to Plain |
| Host answers 502 or 504 under load | longer pause, smaller batches | queue length on the dashboard |
| Required plugin key missing on an order | holds that order | the order and the key |
| ERP rejects or throttles the order | retries with a growing pause | nothing, unless it lasts |
A WordPress update, a plugin update or a host migration is the usual moment for one of these rows. A record that still fails after its retries is kept with its original payload and sent again once the cause is fixed. Before go-live the integration runs as a dry run that writes nothing and shows the ERP orders and customers it would create; after that every run leaves a diff per record. This is how we build every e-commerce ERP integration. The WooCommerce side is on the WooCommerce page; sibling pages are Shopify ERP integration and BigCommerce ERP integration. Tell us which plugins and which ERP you run.
Frequently asked questions
Do we need a plugin in WordPress for the ERP integration?
No. The integration runs outside WordPress and uses the WooCommerce REST API with a consumer key and secret. WordPress needs HTTPS, permalinks set to anything other than Plain, and a dedicated user that owns the key. A key stops working when its user is deleted, which is the usual cause of an integration that stops overnight after a staff change.
How many API calls can WooCommerce handle?
WooCommerce documents no limit for its REST API, so the answer is your host. Each call starts WordPress and loads every active plugin, and it uses one PHP worker while it runs. The integration sends batches of up to 100 objects, only for records that changed, and spaces its calls so checkout keeps its workers.
What happens to orders when the integration endpoint is down?
WooCommerce does not resend a failed webhook, and after 5 consecutive failures it disables the webhook. The integration therefore also polls orders with modified_after and checks that its webhooks are still active. An order a webhook missed arrives with the next poll.
Can the ERP show stock per warehouse in WooCommerce?
WooCommerce core has one stock_quantity per product or variation, an integer, and no location field. The integration adds up the ERP warehouses that may ship web orders, subtracts any buffer you set, rounds decimal quantities down, and writes one number.
Which ERP works best with WooCommerce?
Any ERP whose API exposes the fields you need. The differences are in their limits: NetSuite counts concurrent requests per account, Business Central 6,000 requests per 5 minutes per user, QuickBooks Online 500 per minute per company. SAP Business One and self-hosted ERPNext publish no rate limit, so the server they run on decides.
Our B2B prices come from a plugin. Can the ERP feed it?
Yes, once we know where the plugin stores them. WooCommerce core has no price per customer; B2B plugins keep prices and customer groups in their own meta keys or tables, and some expose them through their own REST endpoints. We read the plugin's storage before go-live and write ERP price levels there.
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