QuickBooks Online (QBO) AI integration expert.
QuickBooks Online (QBO) is the dominant cloud accounting platform for SMBs. It exposes a REST API v3 for programmatic access to the full accounting ledger — customers, vendors, invoices, bills, payments, accounts, reports, and more.
Docs: https://developer.intuit.com/app/developer/qbo/docs/develop
| Role | The Human Decides | The AI Enables |
|---|---|---|
| Business Owner | Strategy, pricing, supplier terms | Cash position summary, overdue receivables alerts, P&L narrative |
| Bookkeeper | Exception review, chart of accounts structure | Auto-categorisation, bank rec suggestions, data entry from documents |
| Accountant | Tax strategy, audit, advisory | Report generation, variance analysis, client-ready narratives |
| Finance Manager | Budget sign-off, forecasting assumptions | Actuals vs budget, scenario modelling from live QBO data |
| Accounts Receivable | Escalations, dispute resolution | Aged AR report, draft payment reminders, invoice status lookup |
| Accounts Payable | Payment approvals, vendor relationships | Bill schedule, cash flow impact, duplicate invoice detection |
QBO uses OAuth 2.0 Authorization Code Flow with a 1-hour access token and 100-day rolling refresh token.
# Environment variables
QBO_CLIENT_ID="ABxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
QBO_CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
QBO_REDIRECT_URI="https://yourapp.com/callback"
QBO_REALM_ID="123456789" # Company ID — obtained from token response
QBO_ACCESS_TOKEN="eyJ..." # Refresh every 60 min
QBO_REFRESH_TOKEN="AB11..." # Rotate every 100 days
QBO_SANDBOX=false # true for development
Authorization: https://appcenter.intuit.com/connect/oauth2
Token: https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer
com.intuit.quickbooks.accounting # All accounting data (required)
com.intuit.quickbooks.payment # Payment processing
curl -X POST https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer \
-H "Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token&refresh_token=AB11..."
# Returns: new access_token + new refresh_token (rolling window resets)
Production: https://quickbooks.api.intuit.com/v3/company/{realmId}/
Sandbox: https://sandbox-quickbooks.api.intuit.com/v3/company/{realmId}/
Always include:
Accept: application/json
Content-Type: application/json
Authorization: Bearer {access_token}
QBO uses a SQL-like query syntax (not standard SQL — it's Intuit's own dialect).
GET /v3/company/{realmId}/query?query=SELECT * FROM Invoice WHERE Balance > '0'
-- Key syntax rules
SELECT * FROM Invoice WHERE TotalAmt > '1000.0' -- single quotes required
SELECT * FROM Customer WHERE DisplayName LIKE 'Acme%' -- % wildcard only
SELECT * FROM Invoice WHERE Balance > '0' ORDER BY DueDate DESC
SELECT * FROM Invoice STARTPOSITION 1 MAXRESULTS 200 -- max 1000 per page
-- Paginate
SELECT COUNT(*) FROM Invoice -- get total first
SELECT * FROM Invoice STARTPOSITION 201 MAXRESULTS 200 -- next page
Critical: values in WHERE clauses must use single quotes. WHERE TotalAmt > 1000 fails silently or errors.
Always pin the API version:
?minorversion=73 # Append to all requests — defaults to 2014 without it
{
"CustomerRef": { "value": "42", "name": "Acme Corp" },
"TxnDate": "2026-04-01",
"DueDate": "2026-04-30",
"Line": [
{
"Amount": 500.00,
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": {
"ItemRef": { "value": "1", "name": "Consulting" },
"Qty": 5,
"UnitPrice": 100.00
}
}
]
}
Create: POST /v3/company/{realmId}/invoice Read: GET /v3/company/{realmId}/invoice/{id} Update: POST /v3/company/{realmId}/invoice (include Id + SyncToken) Send: POST /v3/company/{realmId}/invoice/{id}/send?sendTo=client@example.com
{
"TotalAmt": 500.00,
"CustomerRef": { "value": "42" },
"Line": [{
"Amount": 500.00,
"LinkedTxn": [{ "TxnId": "123", "TxnType": "Invoice" }]
}]
}
{
"VendorRef": { "value": "56" },
"TxnDate": "2026-04-01",
"DueDate": "2026-04-30",
"Line": [{
"Amount": 200.00,
"DetailType": "AccountBasedExpenseLineDetail",
"AccountBasedExpenseLineDetail": {
"AccountRef": { "value": "7", "name": "Office Supplies" }
}
}]
}
{
"DisplayName": "Acme Corp",
"CompanyName": "Acme Corporation",
"PrimaryEmailAddr": { "Address": "accounts@acme.com" },
"BillAddr": { "Line1": "123 Main St", "City": "Cape Town", "CountrySubDivisionCode": "WC" },
"DefaultTaxCodeRef": { "value": "TAX" }
}
AccountType values: Bank | Accounts Receivable | Other Current Asset | Fixed Asset |
Accounts Payable | Credit Card | Equity | Income | Expense | Cost of Goods Sold
| Tool | Description |
|---|---|
qbo_query | Run SQL-like query against any QBO entity |
qbo_get_invoice | Fetch single invoice with full line item detail |
qbo_create_invoice | Create invoice for a customer |
qbo_send_invoice | Email invoice to customer via QBO |
qbo_apply_payment | Apply payment against one or more invoices |
qbo_get_customer | Customer detail including balance and credit limit |
qbo_list_overdue | Invoices past due date with days overdue |
qbo_aged_receivables | AR aging report (Current / 1-30 / 31-60 / 61-90 / 90+) |
qbo_profit_loss | P&L report for a date range |
qbo_balance_sheet | Balance sheet as at a date |
qbo_cash_flow | Cash flow statement |
qbo_cdc | Change data capture — entities modified since a timestamp |
qbo_batch | Submit up to 30 operations in one request |
Reports are not entities — they use a separate endpoint pattern:
# Profit & Loss
GET /v3/company/{realmId}/reports/ProfitAndLoss?start_date=2026-01-01&end_date=2026-03-31
# Balance Sheet
GET /v3/company/{realmId}/reports/BalanceSheet?date_macro=This%20Year-to-date
# Aged Receivables
GET /v3/company/{realmId}/reports/AgedReceivableDetail
# Cash Flow
GET /v3/company/{realmId}/reports/CashFlow?start_date=2026-01-01&end_date=2026-03-31
# Transaction List (detailed ledger)
GET /v3/company/{realmId}/reports/TransactionList?start_date=2026-01-01&end_date=2026-03-31
Reports have a 400,000-cell limit. For large date ranges, break into quarterly or monthly requests and concatenate.
CDC returns all objects of given entity types modified since a timestamp — far more efficient than polling.
GET /v3/company/{realmId}/cdc?entities=Invoice,Payment,Customer&changedSince=2026-04-01T00:00:00Z
Returns a CDCResponse with QueryResponse arrays per entity — including deleted records (status: "Deleted").
CDC + Webhooks pattern (recommended):
Subscribe at: https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks
// Payload arrives as POST to your endpoint
{
"eventNotifications": [{
"realmId": "123456789",
"dataChangeEvent": {
"entities": [{
"name": "Invoice",
"id": "456",
"operation": "Update",
"lastUpdated": "2026-04-01T10:30:00Z"
}]
}
}]
}
Verify the intuit-signature header (HMAC-SHA256 of body with your verifier token):
const sig = request.headers.get('intuit-signature');
const body = await request.text();
const expected = btoa(
String.fromCharCode(...new Uint8Array(
await crypto.subtle.sign('HMAC', verifierKey, new TextEncoder().encode(body))
))
);
if (sig !== expected) return new Response('Unauthorized', { status: 401 });
| Endpoint type | Limit |
|---|---|
| Standard | 500 req/min per realm |
| Batch | 40 req/min per realm |
| Reports | 200 req/min per realm |
| Concurrent | 10 simultaneous per realm+app |
HTTP 429 on breach — implement exponential backoff. Start at 1s, double up to 60s.
Intuit introduced metered pricing for the App Partner Program:
| Tier | Monthly | GET credits included |
|---|---|---|
| Builder (free) | $0 | 500K |
| Silver | $300 | 1M |
| Gold | $1,700 | 10M |
| Platinum | $4,500 | 75M |
Design integrations to minimise unnecessary GET calls — use CDC instead of polling, cache reference data (customers, items, accounts).
Up to 30 operations per batch — useful for bulk imports:
POST /v3/company/{realmId}/batch
{
"BatchItemRequest": [
{
"bId": "1",
"operation": "create",
"Invoice": { ...invoice object... }
},
{
"bId": "2",
"operation": "update",
"Customer": { "Id": "42", "SyncToken": "5", "DisplayName": "Acme Corp Updated" }
}
]
}
SyncToken version number that increments on each change. Always fetch the current object before updating or you'll get a conflict error. Never cache SyncToken.WHERE TotalAmt > '1000' works; WHERE TotalAmt > 1000 does not. This is a silent failure in some cases.?minorversion=73 on every request.TransactionList and BalanceSheet for large date ranges will error. Iterate monthly or quarterly and stitch results.realmId. Multi-tenant apps must store and switch realmId per customer. Never hardcode.status: "Deleted". Always handle this case; entity endpoints return 404 on deleted objects.