Field notes on the QuickBooks Online API
Sparse updates, sync tokens, DisplayName uniqueness, and why posting an invoice is harder than the docs make it look.
Of the three systems in a lead-to-cash chain, QuickBooks Online is consistently the one that takes longest to get right. Not because it's badly designed — it's an accounting system with an accounting system's constraints — but because it behaves unlike the modern REST APIs on either side of it.
These are the things worth knowing before you start, in roughly the order you'll hit them.
Updates replace the whole object unless you say otherwise
A POST to update an entity in QuickBooks is a full update by default. Any field you leave out of the payload is treated as cleared, not as unchanged. Send an invoice update with only the fields you meant to change and you'll wipe everything else on that invoice.
The fix is sparse=true, which switches to a partial update. Use it for every update, and read the current object first anyway, because some fields have interdependencies that a sparse update won't protect you from.
SyncToken is optimistic locking, and it's strict
Every entity carries a SyncToken that increments on each change. Updates must include the current one. Send a stale token and the update is rejected.
This is a good design — it prevents lost updates — but it means you cannot cache entities and write against them later. The pattern that works is read-then-write in a tight window, with a retry that re-reads on a token mismatch. If a human edits the invoice in the QuickBooks UI between your read and your write, you get the mismatch, and the retry is the correct answer rather than an error to surface.
DisplayName uniqueness on customers
QuickBooks enforces that customer DisplayName is unique across the company file. It will reject a create with a duplicate name.
This is the direct cause of the "Acme Inc. 2" records you see in the wild: an integration hits the constraint, catches the error, appends a character, and succeeds. Now the company has two customers.
The correct handling is to treat the duplicate-name error as a signal — it means the customer already exists — and go find it rather than work around it. Search by DisplayName, get the id, use that. And store the id on your side so the next run doesn't repeat the dance.
Note that the name matching is case-insensitive and ignores some punctuation differences, which is helpful for deduplication and unhelpful when two genuinely different customers have near-identical names.
Items carry the GL account
A QuickBooks Item has an IncomeAccountRef — the account revenue posts to. This is why the product mapping matters so much: mapping a Stripe price to the wrong QuickBooks item doesn't fail, it just books the revenue to the wrong account. Nothing errors, the invoice total is correct, and the P&L by product line is wrong.
Two consequences for integration design. First, the mapping table needs to be explicit and reviewable, not inferred from name similarity. Second, an unmapped product should be an exception that stops the posting, not a fallback to a default item — a default item is how everything ends up in "Sales" and the business loses its product-level reporting.
Tax is not your problem, but it will be
TxnTaxDetail controls tax on an invoice. If Stripe already calculated tax and you post the invoice with tax lines, QuickBooks can also apply its own default depending on the customer and item configuration — which double-taxes.
The practical rule: decide which system is authoritative for tax, and be explicit on every write rather than relying on defaults. Explicit means sending the tax code you intend, including when you intend none. Defaults are fine right up until someone changes a company-level setting and every invoice posted after that quietly picks up something different.
And if you're doing anything beyond simple flat rates — US sales tax nexus, VAT, GST — that belongs in a dedicated tax engine, not in your integration.
Payments, fees and payouts
The three-object shape that actually reconciles:
- Payment against the invoice for the full invoice amount, linked via
Line[].LinkedTxnwithTxnType = Invoice - An expense for the processor fee
- A deposit that groups the payments in one payout batch, matching what actually landed in the bank
Skip the fee and the deposit and the customer's bank feed never clears, because Stripe deposited $971 and your books say three invoices totalling $1,000 were paid. This is the single most common complaint about cheap Stripe→QuickBooks tools, and it's why bookkeepers end up doing manual journal entries anyway.
Query language, not REST filtering
QuickBooks uses a SQL-like query endpoint rather than query-string filters. It's fine once you're used to it, with two gotchas: results are paginated with STARTPOSITION and MAXRESULTS (max 1000), and not every field is queryable. Check before you design a lookup around a field.
Minor versions
The API uses a minorversion query parameter for incremental changes. Pin it explicitly. If you don't send one you get a default that can move, which means your integration's behaviour can change without you deploying anything. Pinning and then upgrading deliberately is the whole point of versioning; letting it float is the most avoidable class of production surprise in this chain.
Sandbox behaves differently from production
Rate limits, some validation rules, and the available company-file configuration differ. Things that pass in sandbox can fail in production, most often on tax configuration and on custom fields that don't exist in the real company file.
Get a design partner's real environment early, even read-only. Building entirely against sandbox data is how you discover in week ten that every one of their invoices carries a custom field you didn't model.
The honest summary
None of this is hard. All of it is fiddly, and the failure modes skew toward silent and slightly wrong rather than loud and obviously broken — which is the worst possible property for something posting to a general ledger.
If you're estimating a build that includes QuickBooks, take whatever number you have for the other connectors and roughly double it. That's not pessimism; it's that accounting systems have constraints that exist for good reasons, and every one of them is a thing your code has to respect.
TruelineHQ handles the QuickBooks end — sparse updates, sync tokens, item mapping, fees and payouts — so you don't have to learn any of this. See how it works.
TruelineHQ keeps HubSpot, Stripe Billing and QuickBooks Online in step — the joins, the rules, and the vendor changes. Start in shadow mode and see every write before it happens.