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
--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:
setAuthorizeURLs, and the JWKS URL as a static:
URL mapping
Auth
Accounting requests sendAuthorization: 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.accountingscope.
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: withclient_id,redirect_uri,response_type=code,scope, andstate, it shows the companies to connect. Choosing one redirects withcode,state, andrealmId(only when the accounting or payment scope was asked for). Cancel redirects witherror=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_codeanswers an hour long access token, a refresh token good for 100 days, and withopenidan RS256id_tokenwith Intuit’s issuer,audas an array, andrealmid. Exchanging a code twice revokes what the first exchange issued.- With
grant_type=refresh_tokenthe 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; sendx-include-refresh-token-hard-expires-in: trueforx_refresh_token_hard_expires_in. Setrefresh_rotation_msto 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’ssub, plusemail,givenName,familyName,phoneNumber, andaddressas the scopes allow.- Discovery at
/.well-known/openid_configurationand/.well-known/openid_sandbox_configuration; keys at/op/v1/jwks, sovalidateIdTokenin intuit-oauth succeeds.
Requests and responses
- Responses are JSON. An
Acceptheader that asks for XML only, orformat=xml, answers 406 with an XML fault; an XML request body answers code500. Both messages are Baseline’s own, since the real service would answer XML. minorversionis 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 answers2010.requestidon a POST makes it idempotent: the samerequestidfor the same company replays the first answer, faults included. It may be 50 characters long (36 for batch, wherebIdmay then be 10).- Errors use Intuit’s envelope,
{ "Fault": { "Error": [{ "Message", "Detail", "code", "element" }], "type" }, "time" }, with Intuit’s codes:610object not found,2000unknown entity,2010invalid property,2020required parameter missing,2030invalid id,2050invalid length,2170invalid enumeration,2500invalid reference,4000and4001query errors,5010stale object,6000business validation,6070line amount mismatch,6140duplicate document number,6240duplicate name. Business validation messages are Baseline’s wording. - Every response carries an
intuit_tidheader and atimein Pacific time with an offset. - Entity names in paths match without case (
/Invoice/12and/invoice/12).
Entities
-
POST /v3/company/{realmId}/{entity}— Create or update entity -
GET /v3/company/{realmId}/{entity}/{id}— Read entity -
Updates send
IdandSyncToken."sparse": truechanges only the fields sent; without it, writable fields left out are cleared. ASyncTokenthat is not the current one answers5010. -
?operation=deletedeletes a transaction and answers the{ "status": "Deleted" }stub. List entities (customers, vendors, items, accounts, terms, payment methods) cannot be deleted: setActive: false, which renames them with(deleted). -
?operation=voidvoids an invoice, payment, sales receipt, or credit memo: amounts and quantities go to zero, links are cleared, andPrivateNotestarts withVoided. node-quickbooks voids a payment as an update withinclude=void, which works too. -
MetaDatacarriesCreateTimeandLastUpdatedTime. 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 .../companyinfomakes a sparse updateGET /v3/company/{realmId}/preferences— Read preferences:POST .../preferencesmakes a sparse update
Query
GET /v3/company/{realmId}/query?query=...— Query entities: orPOST .../querywith the statement as anapplication/textbody
- Operators are
=,<,>,<=,>=,IN, andLIKE(with%), joined byAND.OR,NOT,!=, joins, and grouping answer4000with 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), andIdtakes only=andIN. 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. MAXRESULTSdefaults to 100 and is capped at 1000. The response’smaxResultsis the number returned, and an empty result is"QueryResponse": {}.COUNT(*)answerstotalCount.- 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 30BatchItemRequestentries (1040beyond that)
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
{ "status": "Deleted" } stubs, at most 1000 objects. changedSince must be within the last 30 days. When nothing changed, QueryResponse is [{}].
Webhooks
An app with awebhook_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 underaccounts,terms,payment_methods,customers,vendors,items,estimates,invoices,credit_memos,sales_receipts, andpayments. Refs may name their target ({ name: Road bike }) instead of an Id.OpeningBalanceon 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 withfaults to test retries.
Conformance
See CONFORMANCE.md for every provider.