Skip to main content
Business & GrowthAccounting Software272 lines

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

Quick Summary24 lines
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 lines
Paste into your CLAUDE.md or agent config

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 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

  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.

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 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.

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

Get CLI access →

Related Skills