TL;DR: A paid WooCommerce order is a QuickBooks Online sales receipt, not an invoice. Send it through something that holds the OAuth token, and give every create call a requestid so a retry cannot book the sale twice.
- QuickBooks access tokens last 1 hour. Refresh tokens last 100 days, rolling, change value as you use them, and have a hard cap of 5 years.
- Since the Intuit App Partner Program, creating a sales receipt is an unmetered Core call, and querying for one is a metered CorePlus call. Deduplicate with
requestid, not with a lookup. - Limits are 500 requests a minute and 10 a second per company. A throttled call gets a 429, and Intuit says to wait 60 seconds.
- Minor versions 1 to 74 were switched off on 1 August 2025. Every request now runs as
minorversion=75or later. - n8n's QuickBooks node has no sales receipt resource. Use its HTTP Request node with the same QuickBooks credential.
/ Ready-made
Is there an official QuickBooks integration for WooCommerce?
The WooCommerce Marketplace lists QuickBooks Sync for WooCommerce by MyWorks Software, with 10K+ active installs. The plugin is free, but it needs a MyWorks account, which has a free Launch plan and paid plans for higher volume. It syncs orders as an invoice, a sales receipt, a sales order or an estimate, along with payments, refunds, customers, products, tax and inventory, as often as every 5 minutes.¹
That is a lot of bookkeeping that this article does not build. Inventory quantities and refunds alone are weeks of work to get right. If you want every order in QuickBooks with stock kept in step, buy the connector.
Build it yourself when the rules belong to you: only orders from one country, a separate income account per product category, a sales receipt for card orders and an invoice for bank transfers, or the same paid order also going to a CRM and a warehouse, with one delivery log covering all of them.
/ Document type
Should a WooCommerce order become a sales receipt or an invoice?
A sales receipt, for anything paid at checkout. The SalesReceipt reference defines it as being "similar to an invoice", except that the payment is received as part of the sale. An invoice records money the customer still owes, and it stays open until a separate payment is applied to it. Creating invoices for card orders means making two calls per order, or leaving the accountant a pile of open invoices to match up.
Invoices are right where the money really does come later: bank transfer, cash on delivery, or B2B orders on payment terms. The order's payment method is in the webhook payload, so the same workflow can branch on it.
A sales receipt also carries a DepositToAccountRef. Leave it out and the money goes to Undeposited Funds. That is usually what the accountant wants for card payments, because the payout from Stripe or PayPal arrives days later, minus fees, and gets matched there. Ask before you hard-code a bank account.
/ OAuth
Why can't WordPress call the QuickBooks API with a key?
Because QuickBooks Online has no API keys. Every connection is OAuth 2.0, and Intuit's OAuth FAQ sets the lifetimes:
- Access token: 3,600 seconds. A call with an older one returns 401.
- Refresh token: 100 days, rolling. Each use pushes the expiry out again. A store that goes 100 days without an order has to reconnect.
- The refresh token's value changes. Intuit updates it periodically, so you must always store the latest one from the last response.
- Hard cap: 5 years. After that, someone has to click through the consent screen again, whatever the traffic.
The FAQ adds a trap for concurrent code: refresh tokens one at a time. Two refreshes sent with the same refresh token can get the tokens revoked, and the next refresh returns invalid_grant. In WordPress, two queue workers that both notice an expired token is exactly how that happens.
So let a tool that already runs an OAuth client hold the token. n8n's QuickBooks Online credential is OAuth2 and n8n refreshes it, so WordPress only ever sends the order to an n8n webhook URL.
/ Workflow
What does the n8n workflow for a sales receipt look like?
Four nodes: a Webhook node that receives the paid order, a QuickBooks node that finds or creates the customer, a Code node that builds the lines, and an HTTP Request node that creates the sales receipt.
The last one has to be HTTP Request. n8n's QuickBooks node covers bills, customers, employees, estimates, invoices, items, payments, purchases, transactions and vendors, but not sales receipts. In HTTP Request, set Authentication to Predefined Credential Type and choose the same QuickBooks credential, and n8n still refreshes the token for you.
Set the Webhook node to respond When Last Node Finishes. A run that ends in an error then answers 500, and a sender that retries will try again. That retry is safe because of the next section.
JSON — POST /v3/company/{realmId}/salesreceipt?minorversion=75&requestid=wc-1234
{ "CustomerRef": { "value": "58" }, "TxnDate": "2026-11-05", "PrivateNote": "WooCommerce order 1234", "Line": [ { "DetailType": "SalesItemLineDetail", "Amount": 50.00, "Description": "Ceramic mug (x2)", "SalesItemLineDetail": { "ItemRef": { "value": "21" }, "Qty": 2, "UnitPrice": 25.00 } } ] }
Every sales line needs an ItemRef, meaning a product or service that already exists in QuickBooks. You have two options. Map WooCommerce SKUs to QuickBooks item IDs, which means maintaining the map. Or book every line against one generic item, such as "Online sales", and keep the product name in Description. The second loses per-product sales reports in QuickBooks, but it never breaks when someone adds a product. Ask the accountant which one they need.
Leave DocNumber out unless custom transaction numbers are on. The reference says that when the company's CustomTxnNumber preference is off, QuickBooks numbers the receipt itself, and sending a value "can lead to unwanted duplicates". The order number goes in PrivateNote instead.
Outside the US, GlobalTaxCalculation is required, with a value of TaxExcluded, TaxInclusive or NotApplicable. Pick the one that matches how WooCommerce stored the prices, then compare one receipt per tax rate against the order screen before switching it on.
/ Duplicates
How do you stop a retry from creating a second sales receipt?
Send a requestid query parameter that is derived from the order, such as wc-1234. Intuit's API best practices describe it: when a request arrives with a requestid that was already used, the API "returns the response from the original API request" instead of creating a new transaction. Without one, a repeated create makes a duplicate.
It has to be derived from the order, and not generated per attempt. A random ID made inside n8n is new on every run, so a retried delivery would look like a new request. The order ID is the same on attempt 1 and attempt 5.
The obvious alternative is to look the receipt up first, as the Xero version of this workflow does. That costs money now. In Intuit's API classification, Core calls cover "most data-in operations such as creating or updating invoices" and are "unmetered and uncharged". CorePlus calls cover "most data-out operations", including query, and those are metered. A lookup before every create turns a free call into a metered one.
Generate the requestid from the order, never per attempt. A key that changes on every retry is not a key. — the order ID is the idempotency key
| Concern | QuickBooks Sync (MyWorks) | Webhook Actions + n8n |
|---|---|---|
| Inventory, refunds, payments | Built in | Not built. Each one is another workflow you would write |
| Which orders are synced, as what | Its settings | Any condition on the order payload: a country, a payment method, a customer group |
| QuickBooks OAuth tokens | Held by the MyWorks service | Held and refreshed by the n8n credential. WordPress never sees them |
| When the sync runs | As often as every 5 minutes | When the order event fires, queued and retried with backoff |
| QuickBooks down or throttling | See its documentation | n8n answers 500, the delivery is retried, and the same requestid returns the first result |
| One order to several systems | QuickBooks only | The same trigger feeds other webhooks, each with its own log |
Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.
/ Limits
What are the QuickBooks Online API limits for a store?
From Intuit's limits and throttles page, per company (realm ID):
- 500 requests a minute and 10 a second.
- Batch: 40 batch requests a minute per realm and app, with 30 payloads recommended per batch.
- Any request that runs longer than 120 seconds times out.
- A throttled call returns 429, and the page says to "wait 60 seconds before retrying".
At two calls per order (customer, then receipt), 500 ÷ 2 = 250 orders a minute. Few stores ever get close, so the per-second limit matters more: a backlog replayed all at once can go over 10 a second in an instant. Queue it.
The 60-second advice fits a doubling retry schedule well. On a schedule whose first retry waits 60 seconds and then 120, 240 and 480, the first retry already respects the throttle window, and the others wait longer still.
The metered side has its own ceiling. Apps on the free Builder tier include 500,000 CorePlus calls per workspace per month, and calls above that are refused with a 429. This workflow's customer search is a CorePlus call: 500,000 ÷ 30 days ≈ 16,600 orders a day before the customer lookups run out. That is far beyond most stores, but it is a real number if you add a lookup per line item.
/ Versions
What changed with QuickBooks minor versions in 2025?
Intuit's minor versions page: "Support for minor versions 1 through 74 … was discontinued beginning August 1, 2025." A request asking for a lower version gets version 75 anyway, and a request with no minorversion parameter also gets 75. Code written against an older version did not stop working on that date, but it now receives version-75 responses. Put minorversion=75 in the URL so what you test is what runs.
/ Exposure
What does QuickBooks not protect you from?
A dead connection nobody notices. The refresh token lasts 100 days only if it keeps being used. A seasonal shop that sleeps through the winter comes back to invalid_grant. n8n marks the credential as failing, but nothing tells the shop owner. Put an alert on the workflow's error path.
Totals that do not match. QuickBooks recomputes tax from its own tax setup unless you send exactly what WooCommerce charged. A one-cent difference per order is still a reconciliation problem at month end.
Customers that pile up. Creating a customer per guest checkout fills the customer list with one-off buyers. Decide early whether consumer orders share one "Web customer" record.
A failure only the log knows about. When n8n answers 500, the order is still in the delivery log and can be replayed. When n8n itself is down, that log is the only record, which is why the WooCommerce to n8n article argues for a retried, logged delivery instead of a fire-once webhook. The Xero version of this workflow handles the other big accounting API, and the retry policy article explains the schedule.