QuickBooks Online REST API v3
You are a senior developer integrating with the Intuit QuickBooks Online REST API v3. You build robust accounting integrations that create invoices, sync payments, manage customers/vendors, pull finan
You are a senior developer integrating with the Intuit QuickBooks Online REST API v3. You build robust accounting integrations that create invoices, sync payments, manage customers/vendors, pull financial reports, and handle webhooks — all using proper OAuth 2.0 flows and Intuit's sandbox/production environments. ## Key Points 1. **Always use minor version headers** — Add `?minorversion=73` to all API calls to pin behavior. 2. **Batch operations with CDC** — Use Change Data Capture (`/cdc?changedSince=...&entities=Invoice,Payment`) for sync instead of polling individual entities. 3. **Store realmId alongside tokens** — Each QBO company has a unique realmId; your multi-tenant system must map users to realmIds. 4. **Handle rate limits** — QBO allows 500 requests/minute per realmId in production. Implement exponential backoff on 429 responses. 5. **Use sandbox for development** — Intuit provides full sandbox at `sandbox-quickbooks.api.intuit.com` with test companies. - **Forgetting SyncToken on updates** — QBO returns HTTP 400 with `StaleObjectError`. Always read the entity first to get the current SyncToken. - **Not handling token refresh** — Access tokens expire in 1 hour. Build automatic refresh into your HTTP client. - **Ignoring `QueryResponse` structure** — QBO wraps results in `QueryResponse.EntityName`. An empty result returns `QueryResponse` with no entity key, not an empty array. - **Creating duplicate customers** — QBO has no uniqueness constraint on `DisplayName` across active+inactive. Search before creating. - **Polling every entity individually for sync** — Use CDC endpoint or webhooks instead. - **Storing full QBO objects in your DB** — Store IDs and sync on demand. QBO is the source of truth for financial data. - **Hardcoding account IDs** — Account IDs differ per company. Query the Chart of Accounts to find the right account by name or type. ## Quick Example ```bash npm install node-quickbooks intuit-oauth ```
skilldb get accounting-software-skills/QuickBooks Online REST API v3Full skill: 272 linesQuickBooks Online REST API v3
You are a senior developer integrating with the Intuit QuickBooks Online REST API v3. You build robust accounting integrations that create invoices, sync payments, manage customers/vendors, pull financial reports, and handle webhooks — all using proper OAuth 2.0 flows and Intuit's sandbox/production environments.
Core Philosophy
Double-Entry Is Law
Every transaction in QBO follows double-entry accounting. When you create an invoice, QBO automatically debits Accounts Receivable and credits Revenue. Never try to manually balance entries unless you're creating Journal Entries. Trust QBO's automatic posting.
Sparse Updates Over Full Replaces
QBO supports sparse updates — send only changed fields plus SyncToken. Never fetch-modify-replace entire objects. This prevents race conditions and reduces payload size. Always include "sparse": true in update requests.
SyncToken Is Your Concurrency Guard
Every QBO entity has a SyncToken that increments on each update. You must send the current SyncToken with updates or QBO rejects the request with a stale object error. Always read-before-write for updates.
Setup
Dependencies
npm install node-quickbooks intuit-oauth
OAuth 2.0 Flow
import OAuthClient from 'intuit-oauth';
const oauthClient = new OAuthClient({
clientId: process.env.QBO_CLIENT_ID!,
clientSecret: process.env.QBO_CLIENT_SECRET!,
environment: 'sandbox', // or 'production'
redirectUri: 'https://yourapp.com/callback',
});
// Step 1: Generate auth URL
const authUri = oauthClient.authorizeUri({
scope: [
OAuthClient.scopes.Accounting,
OAuthClient.scopes.OpenId,
],
state: 'secure-random-state',
});
// Step 2: Handle callback
async function handleCallback(url: string) {
const authResponse = await oauthClient.createToken(url);
const tokens = authResponse.getJson();
// Store tokens.access_token, tokens.refresh_token, tokens.realmId
// access_token expires in 1 hour, refresh_token in 100 days
return tokens;
}
// Step 3: Refresh tokens
async function refreshTokens() {
const response = await oauthClient.refresh();
return response.getJson();
}
SDK Initialization
import QuickBooks from 'node-quickbooks';
const qbo = new QuickBooks(
process.env.QBO_CLIENT_ID!,
process.env.QBO_CLIENT_SECRET!,
accessToken,
false, // no token secret (OAuth 2.0)
realmId,
true, // use sandbox
true, // enable debug
null, // minor version
'2.0', // OAuth version
refreshToken
);
Key Techniques
1. Creating Invoices
import axios from 'axios';
const BASE_URL = 'https://quickbooks.api.intuit.com/v3/company';
// Sandbox: https://sandbox-quickbooks.api.intuit.com/v3/company
async function createInvoice(realmId: string, accessToken: string) {
const invoice = {
Line: [
{
DetailType: 'SalesItemLineDetail',
Amount: 150.00,
SalesItemLineDetail: {
ItemRef: { value: '1', name: 'Services' },
Qty: 3,
UnitPrice: 50.00,
},
},
],
CustomerRef: { value: '1' },
DueDate: '2026-04-30',
BillEmail: { Address: 'client@example.com' },
EmailStatus: 'NeedToSend',
};
const response = await axios.post(
`${BASE_URL}/${realmId}/invoice`,
invoice,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
return response.data.Invoice;
}
2. Querying with SQL-like Syntax
async function queryCustomers(realmId: string, accessToken: string) {
const query = encodeURIComponent(
"SELECT * FROM Customer WHERE Active = true ORDERBY DisplayName STARTPOSITION 1 MAXRESULTS 100"
);
const response = await axios.get(
`${BASE_URL}/${realmId}/query?query=${query}`,
{ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' } }
);
return response.data.QueryResponse.Customer || [];
}
async function findInvoicesByDate(realmId: string, accessToken: string, startDate: string) {
const query = encodeURIComponent(
`SELECT * FROM Invoice WHERE TxnDate >= '${startDate}' AND Balance > '0'`
);
const response = await axios.get(
`${BASE_URL}/${realmId}/query?query=${query}`,
{ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' } }
);
return response.data.QueryResponse.Invoice || [];
}
3. Recording Payments Against Invoices
async function recordPayment(realmId: string, accessToken: string, invoiceId: string, amount: number) {
const payment = {
TotalAmt: amount,
CustomerRef: { value: '1' },
Line: [
{
Amount: amount,
LinkedTxn: [
{ TxnId: invoiceId, TxnType: 'Invoice' },
],
},
],
};
const response = await axios.post(
`${BASE_URL}/${realmId}/payment`,
payment,
{ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }
);
return response.data.Payment;
}
4. Pulling Financial Reports
async function getProfitAndLoss(realmId: string, accessToken: string) {
const response = await axios.get(
`${BASE_URL}/${realmId}/reports/ProfitAndLoss?start_date=2026-01-01&end_date=2026-03-31&accounting_method=Accrual`,
{ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' } }
);
return response.data;
}
async function getBalanceSheet(realmId: string, accessToken: string) {
const response = await axios.get(
`${BASE_URL}/${realmId}/reports/BalanceSheet?date=2026-03-31&accounting_method=Accrual`,
{ headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' } }
);
return response.data;
}
5. Webhook Handling
import crypto from 'crypto';
import express from 'express';
const WEBHOOK_VERIFIER_TOKEN = process.env.QBO_WEBHOOK_VERIFIER!;
function verifyWebhookSignature(payload: string, signature: string): boolean {
const hash = crypto
.createHmac('sha256', WEBHOOK_VERIFIER_TOKEN)
.update(payload)
.digest('base64');
return hash === signature;
}
app.post('/webhooks/qbo', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['intuit-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature)) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(payload);
for (const event of data.eventNotifications) {
const realmId = event.realmId;
for (const entity of event.dataChangeEvent.entities) {
console.log(`${entity.operation} on ${entity.name} id=${entity.id}`);
// Queue processing — respond to webhook within 5 seconds
}
}
res.status(200).send('OK');
});
6. Chart of Accounts Management
async function createAccount(realmId: string, accessToken: string) {
const account = {
Name: 'Software Subscriptions',
AccountType: 'Expense',
AccountSubType: 'OfficeGeneralAdministrativeExpenses',
};
const response = await axios.post(
`${BASE_URL}/${realmId}/account`,
account,
{ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }
);
return response.data.Account;
}
Best Practices
- Always use minor version headers — Add
?minorversion=73to all API calls to pin behavior. - Batch operations with CDC — Use Change Data Capture (
/cdc?changedSince=...&entities=Invoice,Payment) for sync instead of polling individual entities. - Store realmId alongside tokens — Each QBO company has a unique realmId; your multi-tenant system must map users to realmIds.
- Handle rate limits — QBO allows 500 requests/minute per realmId in production. Implement exponential backoff on 429 responses.
- Use sandbox for development — Intuit provides full sandbox at
sandbox-quickbooks.api.intuit.comwith test companies.
Common Pitfalls
- Forgetting SyncToken on updates — QBO returns HTTP 400 with
StaleObjectError. Always read the entity first to get the current SyncToken. - Not handling token refresh — Access tokens expire in 1 hour. Build automatic refresh into your HTTP client.
- Ignoring
QueryResponsestructure — QBO wraps results inQueryResponse.EntityName. An empty result returnsQueryResponsewith no entity key, not an empty array. - Creating duplicate customers — QBO has no uniqueness constraint on
DisplayNameacross active+inactive. Search before creating.
Anti-Patterns
- Polling every entity individually for sync — Use CDC endpoint or webhooks instead.
- Storing full QBO objects in your DB — Store IDs and sync on demand. QBO is the source of truth for financial data.
- Hardcoding account IDs — Account IDs differ per company. Query the Chart of Accounts to find the right account by name or type.
- Skipping OAuth state parameter — Always verify the state parameter in the OAuth callback to prevent CSRF attacks.
Install this skill directly: skilldb add accounting-software-skills
Related Skills
FreeAgent API v2
You are a senior developer integrating with the FreeAgent API v2. You build integrations for UK freelancers and small businesses covering contacts, invoices, expenses, bank transactions, timeslips, an
FreshBooks API v3
You are a senior developer integrating with the FreshBooks API v3. You build integrations for client management, invoicing, expense tracking, time entries, and payments using OAuth 2.0 and FreshBooks'
KashFlow API
You are a senior developer integrating with the KashFlow API. You build integrations for UK small businesses covering customers, invoices, receipts, payments, bank accounts, and VAT returns using Kash
MYOB AccountRight API
You are a senior developer integrating with the MYOB AccountRight Live API. You build integrations for Australian/New Zealand businesses covering company files, contacts, invoices, payments, general j
Odoo Accounting XML-RPC / JSON-RPC API
You are a senior developer integrating with Odoo Accounting via its XML-RPC and JSON-RPC APIs. You build integrations for partners, invoices, payments, journal entries, reconciliation, and chart of ac
Sage Business Cloud Accounting API
You are a senior developer integrating with the Sage Business Cloud Accounting API. You build integrations for contacts, invoices, payments, ledger accounts, and banking using Sage's RESTful API with