verification
use it
# add to stunt.yaml
services:
qbo:
adapter: embedded:qbo-style
# then
$ stunt upThe embedded: source extracts this adapter from the binary — no git clone, fully offline.
how to mock the QuickBooks Online API locally
Install stunt, paste the service block above into stunt.yaml, run stunt up — then point your client at localhost instead of QuickBooks Online API. It works offline, keeps state across requests and restarts, and every response is synthetic: the route shapes, status codes, pagination, and webhook delivery follow the real QuickBooks Online API v3 surface — the data is fake, the behavior is real.
backing stores
oauth_codesaccess_tokensrefresh_tokenscustomersinvoices
api surface (11)
generated from the adapter manifest — exactly what boots, nothing more
11 routes
GET/oauth/v2/authorize
POST/oauth/v2/tokens/bearer
GET/v3/company/{realmId}/query
POST/v3/company/{realmId}/query
GET/v3/company/{realmId}/customer
POST/v3/company/{realmId}/customer
GET/v3/company/{realmId}/customer/{id}
DELETE/v3/company/{realmId}/customer/{id}
POST/v3/company/{realmId}/invoice
GET/v3/company/{realmId}/invoice/{id}
DELETE/v3/company/{realmId}/invoice/{id}
VM-verified behaviors (23)
- ✓authorize without client_id/redirect_uri/state is invalid_request
- ✓authorize 302s back to the redirect_uri with code, state and a fresh realmId
- ✓the auth code exchanges once into a token pair; reuse is invalid_grant
- ✓refresh rotates the refresh token and kills the old one (the QBO churn)
- ✓unknown grants and unknown codes are OAuth 400s
- ✓v3 calls demand a live bearer: absent, unknown and expired tokens are 401 fault 32001
- ✓the bearer is realm-bound: another company's path is 401
- ✓DisplayName is required: a 400 fault 610
- ✓customer create assigns Id, SyncToken 0 and QBO defaults
- ✓reads by path id and ?id= round-trip; unknown ids are 404 fault 620
- ✓POST with Id updates: a sparse merge bumps SyncToken and leaves unspecified fields alone
- ✓DELETE deactivates: the record stays readable by id but leaves the active list
- ✓invoice create prices its Line items into TotalAmt/Balance and mints a DocNumber
- ✓a Line is required: a 400 fault 610
- ✓void flips status Voided and zeroes Balance, keeping the record
- ✓DELETE is a hard delete: the record is gone afterwards
- ✓entity detection follows the FROM token, so CustomerRef.value reads Invoices
- ✓WHERE filters by equality, LIKE and IN over stored fields
- ✓ORDER BY DESC with MAXRESULTS caps the page and reports maxResults
- ✓the POST query endpoint takes the statement from the body
- ✓default reads hide deactivated customers; WHERE Active = False surfaces them
- ✓an unknown entity answers an empty QueryResponse; a statement without one is a 400 fault
- ✓STARTPOSITION pages with MAXRESULTS: row 2 of the sorted set
Not implemented (6)
- Only Customer and Invoice — no Payment, Estimate, Item, Vendor, etc.
- No QSQL aggregates (COUNT, SUM, MAX, MIN, AVG)
- No batch endpoint (POST /v3/company/{realmId}/batch)
- No CDC endpoint (GET /v3/company/{realmId}/cdc)
- No reports endpoints (Profit and Loss, Balance Sheet, Trial Balance)
- No Intuit webhooks (QBO event notifications)
Differs from the real API (4)
- Malformed FROM tokens fall back to substring entity matching instead of erroring
- Full (non-sparse) update merges like sparse; real QBO replaces and clears omitted fields
- SyncToken optimistic concurrency is not enforced — stale tokens still update
- Data is not realm-partitioned: one realm's token reads another realm's records
want more of the QuickBooks Online API surface? adapters are YAML + Starlark — the authoring guide covers adding routes, fixtures, and state machines. Open an issue for anything big first, or just file a PR — merged routes ship in the next release.