Skip to main content
Stateful QuickBooks Online emulation of the Accounting API v3 at minor version 75. It covers customers (with jobs), vendors, items (inventory, non-inventory, and service), accounts, terms, and payment methods; invoices, estimates, payments, credit memos, and sales receipts, with create, read, full and sparse update, delete, and void; SyncToken checks; and balances that stay consistent: invoice balances, customer balances, A/R, deposit accounts, and inventory quantity and value are recomputed on every write. It also covers the query language, CompanyInfo and Preferences, batch, change data capture, requestid replay, Intuit’s fault envelope, OAuth 2.0 with a company picker and rotating refresh tokens, OpenID userinfo and ID tokens, and webhooks signed with intuit-signature. The tests drive it with node-quickbooks (2.0.50) and Intuit’s intuit-oauth (4.2.5).

At a glance

Start

QuickBooks starts only when named in config or with --service. When it is the only enabled service, it listens on http://localhost:4000. Without seed config, one company (realm 9341453050298367) starts with the default chart and lists, and a built-in app is available with client ID ABbaselineClientId0000000000000000000000000000000, secret baselineClientSecret00000000000000000000, and any redirect URI. node-quickbooks keeps the API base on the instance, and its token URLs are statics:
intuit-oauth takes the OAuth endpoints through setAuthorizeURLs, and the JWKS URL as a static:

URL mapping

Auth

Accounting requests send Authorization: Bearer <token>.
  • A token the CLI accepts (for example test_token_admin) may call every company.
  • tokens[] seeds access tokens that do not expire, each for one company (realm_id).
  • OAuth access tokens last an hour and call the company chosen on the connect page. They need the com.intuit.quickbooks.accounting scope.
A missing, unknown, expired, or revoked token answers 401 with code 3200. A token for another company, or one without the accounting scope, answers 403 with code 3100. Both use Intuit’s gateway layout (a lowercase fault.error[] body), as the real service does.

OAuth 2.0

  • GET /connect/oauth2 — Connect page: with client_id, redirect_uri, response_type=code, scope, and state, it shows the companies to connect. Choosing one redirects with code, state, and realmId (only when the accounting or payment scope was asked for). Cancel redirects with error=access_denied.
  • POST /oauth2/v1/tokens/bearer — Get tokens: takes the client credentials as Basic auth (or in the body) and a form body. grant_type=authorization_code answers an hour long access token, a refresh token good for 100 days, and with openid an RS256 id_token with Intuit’s issuer, aud as an array, and realmid. Exchanging a code twice revokes what the first exchange issued.
  • With grant_type=refresh_token the same call answers a new access token. Within 24 hours of a refresh token’s issue it keeps its value and the 100 days roll forward; after that it rotates and the old value stops working. Refresh tokens expire outright after five years; send x-include-refresh-token-hard-expires-in: true for x_refresh_token_hard_expires_in. Set refresh_rotation_ms to shorten the 24 hours in tests.
  • POST /v2/oauth2/tokens/revoke — Revoke tokens: with { "token": ... } (JSON or a form) it ends the whole connection, both tokens.
  • GET /v1/openid_connect/userinfo — Get user info: answers the company owner’s sub, plus email, givenName, familyName, phoneNumber, and address as the scopes allow.
  • Discovery at /.well-known/openid_configuration and /.well-known/openid_sandbox_configuration; keys at /op/v1/jwks, so validateIdToken in intuit-oauth succeeds.

Requests and responses

  • Responses are JSON. An Accept header that asks for XML only, or format=xml, answers 406 with an XML fault; an XML request body answers code 500. Both messages are Baseline’s own, since the real service would answer XML.
  • minorversion is optional. Values below 75 and a missing value are served as 75, as Intuit does since August 2025. A value that is not a number answers 2010.
  • requestid on a POST makes it idempotent: the same requestid for the same company replays the first answer, faults included. It may be 50 characters long (36 for batch, where bId may then be 10).
  • Errors use Intuit’s envelope, { "Fault": { "Error": [{ "Message", "Detail", "code", "element" }], "type" }, "time" }, with Intuit’s codes: 610 object not found, 2000 unknown entity, 2010 invalid property, 2020 required parameter missing, 2030 invalid id, 2050 invalid length, 2170 invalid enumeration, 2500 invalid reference, 4000 and 4001 query errors, 5010 stale object, 6000 business validation, 6070 line amount mismatch, 6140 duplicate document number, 6240 duplicate name. Business validation messages are Baseline’s wording.
  • Every response carries an intuit_tid header and a time in Pacific time with an offset.
  • Entity names in paths match without case (/Invoice/12 and /invoice/12).

Entities

  • POST /v3/company/{realmId}/{entity} — Create or update entity
  • GET /v3/company/{realmId}/{entity}/{id} — Read entity
  • Updates send Id and SyncToken. "sparse": true changes only the fields sent; without it, writable fields left out are cleared. A SyncToken that is not the current one answers 5010.
  • ?operation=delete deletes a transaction and answers the { "status": "Deleted" } stub. List entities (customers, vendors, items, accounts, terms, payment methods) cannot be deleted: set Active: false, which renames them with (deleted).
  • ?operation=void voids an invoice, payment, sales receipt, or credit memo: amounts and quantities go to zero, links are cleared, and PrivateNote starts with Voided. node-quickbooks voids a payment as an update with include=void, which works too.
  • MetaData carries CreateTime and LastUpdatedTime. Ref names (CustomerRef.name, ItemRef.name) are filled on every read.
DocNumber is numbered from 1001 unless Preferences.SalesFormsPrefs.CustomTxnNumbers is on. With custom numbers, a duplicate answers 6140 unless the request sends include=allowduplicatedocnum. New companies start with a chart of accounts (Checking, Savings, Accounts Receivable, Undeposited Funds, Inventory Asset, Accounts Payable, Opening Balance Equity, Retained Earnings, Sales of Product Income, Services, Discounts given, Cost of Goods Sold, Office Expenses), the terms Due on receipt, Net 15, Net 30 (the default), and Net 60, the payment methods Cash, Check, Visa, MasterCard, and American Express, and the items Services and Hours.

Company

  • GET /v3/company/{realmId}/companyinfo/{realmId} — Read company info: POST .../companyinfo makes a sparse update
  • GET /v3/company/{realmId}/preferences — Read preferences: POST .../preferences makes a sparse update

Query

  • GET /v3/company/{realmId}/query?query=... — Query entities: or POST .../query with the statement as an application/text body
  • Operators are =, <, >, <=, >=, IN, and LIKE (with %), joined by AND. OR, NOT, !=, joins, and grouping answer 4000 with a parser message naming the line and column. Strings are single-quoted, with \' for a quote.
  • An unknown property answers 4001 (property 'X' is not queryable), and Id takes only = and IN. Refs compare by their value, string comparison ignores case, and dates and times compare as instants.
  • Lists return only active entries unless the query filters on Active.
  • MAXRESULTS defaults to 100 and is capped at 1000. The response’s maxResults is the number returned, and an empty result is "QueryResponse": {}. COUNT(*) answers totalCount.
  • A field list (SELECT Id, DisplayName FROM Customer) is accepted and checked, and whole objects are returned.

Batch

  • POST /v3/company/{realmId}/batch — Batch request: up to 30 BatchItemRequest entries (1040 beyond that)
Each has a unique bId and is a Query or an entity with operation create, update, delete, or void. Items run in order and on their own: a failed item answers its Fault under its bId and the rest still run.

Change data capture

  • GET /v3/company/{realmId}/cdc?entities=Invoice,Customer&changedSince=2026-10-01T00:00:00-07:00 — Change data capture
It answers every object of those types changed since then, deleted transactions as { "status": "Deleted" } stubs, at most 1000 objects. changedSince must be within the last 30 days. When nothing changed, QueryResponse is [{}].

Webhooks

An app with a webhook_url hears changes to the companies it is connected to (an unrevoked OAuth connection) and the companies in its webhook_realms (every company when left out), for the entities in webhook_entities (every entity when left out). The changes one request makes go out together, in one POST per app. The body is CloudEvents by default (a JSON array, Intuit’s format since May 2026):
webhook_format: legacy sends the older eventNotifications envelope instead. Either way the intuit-signature header is the base64 HMAC-SHA256 of the raw body keyed with the app’s verifier_token. Requests also carry intuit-created-time, intuit-t-id, intuit-notification-schema-version: 0.1, and User-Agent: intuit_notification_server/0.1. A delivery that does not answer 2xx within three seconds is retried after 1, 2, and 3 seconds.

Seed config

  • companies[] take entities in API form under accounts, terms, payment_methods, customers, vendors, items, estimates, invoices, credit_memos, sales_receipts, and payments. Refs may name their target ({ name: Road bike }) instead of an Id. OpeningBalance on a balance sheet account sets its starting balance.

Inspector

GET /_inspector has tabs for the company (with a company switcher), customers and vendors, items, accounts, transactions, and apps with their connections and webhook deliveries.

Limits

Reports, attachments and uploads, PDFs and email sending, bills, purchases, journal entries, deposits, transfers, classes, departments, tax codes and sales tax, employees and time activities, multicurrency, recurring transactions, entity merges, the Payments API, and XML. Baseline never rate limits; inject Intuit’s errors with faults to test retries.

Conformance

See CONFORMANCE.md for every provider.