Service · QuickBooks Online · API

QuickBooks Online API,
done properly.

QBO's API is powerful and weird in equal measure. The OAuth flow is unusually involved. Half the fields you'd expect to query aren't queryable. Sync tokens drift if you handle writes wrong. We've shipped a production QBO integration that writes to ~50 invoices an hour without a single sync conflict. This is what it looks like.

QBO writes per hour
~50
Sync conflicts
0
Backfill match rate
94%
API version
v3 · m75
The OAuth dance

One-time browser flow. Refresh tokens forever.

QBO uses the standard OAuth 2 authorization-code grant with refresh tokens. The catch for headless automation: you must complete the initial authorization in a browser, once, signing in as the Intuit user who owns the company file.

The pattern we ship is a one-shot bootstrap script that pops a local browser, runs the consent flow, captures the authorization code on a localhost callback, exchanges it for the first access + refresh token pair, and writes them to disk encrypted. Every automation run loads the refresh token, mints a fresh access token (~1 hour TTL), issues its queries, and rotates the refresh token on disk.

After the one-time bootstrap, the integration runs unattended. We've had production runs go 11 months between manual interventions.

scripts/qb_oauth_bootstrap.py · one-time browser flow python
python -m scripts.qb_oauth_bootstrap

# opens browser to Intuit consent screen
# user signs in once, grants access
# script captures code on http://localhost:8000/cb
# exchanges for tokens, writes encrypted to cache/qbo/tokens.json
# done. every subsequent run reads from disk.
The gotcha that ate half a day

PrivateNote is not queryable.

The QBO API supports a SQL-like query language. Most fields you'd expect to filter on work as expected — DocNumber, CustomerRef, MetaData.CreateTime, TotalAmt. Then there's PrivateNote.

PrivateNote is the obvious field to use as a bridge between QBO invoices and other systems (Shopify, your CRM, anywhere you need a free-form identifier). It's projectable — you can SELECT PrivateNote FROM Invoice and get the value back. It is not queryable. The moment you write WHERE PrivateNote = 'RB32708', you get a 400.

QBO error response · the surprise
$ curl -X POST https://quickbooks.api.intuit.com/v3/.../query \
-H 'Authorization: Bearer ...' \
-d "SELECT * FROM Invoice WHERE PrivateNote = 'RB32708'"
{
"Fault": {
"Error": [{
"Message": "QueryValidationError",
"Detail": "Property: PrivateNote is not queryable.",
"code": "4001"
}]
}
}

Intuit's docs don't make this obvious. You discover it at runtime, after writing the naive integration. The fix is an in-memory index, built once per run, against a date- bounded invoice pull. We've used this pattern at production scale for months — it dodges the queryability gap entirely and adds maybe 5 seconds to a run.

Sparse updates + sync tokens

How to write to QBO without race conditions.

Every QBO entity carries a SyncToken. You can't write to an entity without sending the current SyncToken back; QBO uses it to detect concurrent edits. Sparse updates (the "partial PATCH" pattern) make this manageable — but only if you actually structure your writes for them.

The default Intuit pattern is "full update": fetch the whole invoice, modify the one field you care about, send the whole thing back. It works. It also writes to every field, advances the sync token, and turns every minor change into a major audit-log entry.

Sparse updates take a single field, a SyncToken, and an sparse: true flag. QBO updates only that field, advances the token, and leaves everything else untouched.

shared/quickbooks_client.py · sparse update pattern python
def set_tracking_number(invoice_id, sync_token, tracking_num):
    payload = {
        "Id": invoice_id,
        "SyncToken": sync_token,
        "sparse": True,
        "TrackingNum": tracking_num,
    }
    response = self.post(f"/invoice", json=payload)
    return response.json()["Invoice"]   # new sync token comes back
What we'll build for you

Specific QBO integrations we ship.

Sync

ShipStation tracking → QBO

Hourly auto-sync of tracking numbers from labels to invoices, idempotent, multi-store safe.

Sync

Shopify order → QBO invoice

If your existing sync tool falls over edge cases (partial payments, refunds, multi-currency), a direct custom sync handles it.

Bulk ops

Batch purchase orders

Tag, set ship date, fetch PDFs, email the warehouse — for hundreds of invoices, scriptable, repeatable.

Cost

Supplier price list → PurchaseCost

Drop supplier file on dashboard → diff against current PurchaseCost → write only changes via sparse update.

Reporting

Custom QBO data extracts

Now that the auth and querying infrastructure exist, building targeted reports is days, not weeks.

Migration

Replace Xero / MYOB exports

If you're moving to QBO from another system, we can script the migration end-to-end with reconciliation reports.

Why we built this expertise

Real production, not tutorials.

This isn't lifted from a Stack Overflow answer. The patterns on this page came out of shipping a real production integration for Raw Blend — a multi-store Shopify business writing ~50 invoice updates per hour against QBO, across six storefronts, with zero sync conflicts to date.

If your business runs on QuickBooks Online and you need it to do something it doesn't do out of the box — talk to us. We've already paid the discovery costs that eat up most projects.

Related Builds

Other angles on the
same platform.

Every page on this list is drawn from the same Raw Blend build — different problems, same operating model.

Got a QuickBooks integration
you can't buy off the shelf?

We ship custom QBO integrations for Australian businesses — Shopify, ShipStation, Xero migrations, supplier feeds, anywhere QBO needs to talk to a system it doesn't natively support. Discovery is free.

Discovery is free · Quote in 48 hours · No retainer required