# QuickBooks Online simulator

A local QuickBooks Online for accounting code: customers, list entities, fourteen transactions and CompanyInfo.

`quickbooks-twin` is a local, stateful simulator of the QuickBooks Online Accounting API for testing accounting code. Customers, the list entities, fourteen transactions and the company's CompanyInfo keep QuickBooks' envelopes, `SyncToken` checks and faults, in JSON at minorversion 75. Expect QuickBooks Online's own field defaults, fault codes and update rules everywhere except where a **Differs from QuickBooks** line or [Limitations](#limitations) names a gap. It never calls Intuit, moves no money and sends no email, so no test record lands in a real company file.

## Quickstart

<!-- BEGIN generated: quickstart (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

Install the CLI, sign in and boot the simulator with `RYSTIC_URL` pointing at it.

On Pro you sign in with an API key from [your account](https://www.rystic.ai/account/api-keys).

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…   # your API key
export RYSTIC_URL=$(rystic run quickbooks-twin -d --port 0 --scenario quickbooks_customer_ledger --json | jq -r .data.url)
curl -s "$RYSTIC_URL/_rystic/version"
# -> {..."product":"quickbooks-twin"...}
```

On Enterprise, the licensed edition signs in with a license file in place of the key: `rystic login ./rystic-license.json` ([licensing](https://www.rystic.ai/docs/licensing)).

<!-- END generated: quickstart -->

### First calls

Every path names the default scenario's realm, `4620816365320737870`. Read a customer, change one field with a sparse update that names its current `SyncToken` and read it back:

```bash
R=4620816365320737870
curl -s "$RYSTIC_URL/v3/company/$R/customer/1" -H 'Authorization: Bearer placeholder' | jq -c '.Customer | {Id, DisplayName, Balance, SyncToken}'
# -> {"Id":"1","DisplayName":"Amy's Bird Sanctuary","Balance":239,"SyncToken":"3"}
curl -s "$RYSTIC_URL/v3/company/$R/customer?minorversion=75" -H 'Authorization: Bearer placeholder' -H 'Content-Type: application/json' -d '{"Id":"1","SyncToken":"3","sparse":true,"Notes":"Prefers morning delivery"}' | jq -c '.Customer | {Id, Notes, SyncToken}'
# -> {"Id":"1","Notes":"Prefers morning delivery","SyncToken":"4"}
curl -s "$RYSTIC_URL/v3/company/$R/customer/1" -H 'Authorization: Bearer placeholder' | jq -c '.Customer | {Id, Notes, SyncToken}'
# -> {"Id":"1","Notes":"Prefers morning delivery","SyncToken":"4"}
```

## Connect your client

Point your client at `$RYSTIC_URL` in place of `https://sandbox-quickbooks.api.intuit.com` or `https://quickbooks.api.intuit.com`; the `/v3/company/{realmId}/…` paths stay as they are, and any non-empty access token works. Node, with `node-quickbooks`:

```js
const QuickBooks = require("node-quickbooks");

QuickBooks.V3_ENDPOINT_BASE_URL = `${process.env.RYSTIC_URL}/v3/company/`;
const qbo = new QuickBooks(
  "placeholder", "placeholder", "placeholder", false,
  "4620816365320737870", true, false, 75, "2.0", "placeholder",
);
qbo.getCustomer("1", (err, customer) => {
  if (err) throw new Error(JSON.stringify(err));
  console.log(customer.DisplayName);
});
```

Python, with `python-quickbooks`:

```python
import os

from intuitlib.client import AuthClient
from quickbooks import QuickBooks
from quickbooks.objects.customer import Customer

os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
QuickBooks.sandbox_api_url_v3 = os.environ["RYSTIC_URL"] + "/v3"
auth = AuthClient("placeholder", "placeholder", "http://localhost/callback", "sandbox")
auth.access_token = "placeholder"
auth.refresh_token = "placeholder"
client = QuickBooks(auth_client=auth, company_id="4620816365320737870", minorversion=75)
print(Customer.get(1, qb=client).DisplayName)
```

The simulator serves plain HTTP, which `python-quickbooks` refuses without `OAUTHLIB_INSECURE_TRANSPORT`. For another library, set its base URL as [Point your client at it](https://www.rystic.ai/docs/installation.md#point-your-client-at-it) describes. Without the [intercept gateway](https://www.rystic.ai/docs/intercept.md), a library that keeps Intuit's host talks to Intuit.

<!-- BEGIN generated: known-clients (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

No client has been checked against this simulator yet.

<!-- END generated: known-clients -->

## Auth

Send `Authorization: Bearer <token>` with any non-empty OAuth 2.0 token.

A request with no bearer token returns 401 with code `3200` and type `AuthenticationFault`. That covers no `Authorization` header, an empty token or another scheme such as `Basic`. The envelope depends on the route, so assert on the status and the code only:

| Route | 401 body |
|---|---|
| a customer, bill, sales receipt or CompanyInfo read | a lowercase `fault` beside empty response slots, with `time` in Unix milliseconds, as below |
| most other routes | a capitalized `Fault` with an RFC 3339 `time` |

```json
{"attachableResponse":[],"batchItemResponse":[],"cdcresponse":[],"fault":{"error":[{"code":"3200","detail":"Token expired or invalid","element":null,"message":"AuthenticationFailed"}],"type":"AuthenticationFault"},"intuitObject":null,"queryResponse":null,"report":null,"requestId":null,"status":null,"syncErrorResponse":null,"time":1768780800001,"warnings":null}
```

No OAuth route is served: send a placeholder in place of minting a token. Because the simulator checks no token, it cannot exercise your token-refresh path.

**Differs from QuickBooks:** the simulator never refuses a token as expired, revoked or short of a scope. Its only 403 is a CompanyInfo read of a realm with no seeded company.

## API version

The simulator serves every request as minorversion 75, reads no `minorversion` parameter and sends no version header. A request that names another value, or none, gets the same shapes. Pass `minorversion=75` in your client anyway, as both libraries above do, so your code asks for what the simulator serves.

## Errors and limits

### Error format

Most refusals return a 400 whose body is a `Fault` holding `type` and an `Error` array, beside a `time`. Each error has `code`, `Message`, `Detail` and `element`, the field it names or empty. Assert on the status, `type` and `code` rather than the `Detail` text. A behavior marked (unconfirmed) has not been checked against QuickBooks. How a read refuses an `Id`:

| Read | Response |
|---|---|
| a numeric `Id` that matches no record, on any read but TaxCode, TaxRate and [CompanyInfo](#seeding-state) | 400 `ValidationFault` code `610`, `Message` `Object Not Found` |
| a TaxCode or TaxRate `Id` that matches no record | 404 with an empty body (unconfirmed) |
| a non-numeric `Id` on PaymentMethod or PurchaseOrder | 400 `ValidationFault` code `610` |
| a non-numeric `Id` on any other read | 404 with an empty body (unconfirmed) |

A write body that does not parse as JSON returns 400 `ValidationFault` code `6240`, before the token is checked. Its body is the duplicate-name fault, `Message` `Duplicate Name Exists Error`, with a fixed `time`; whether QuickBooks returns the same fault is unconfirmed.

[An unserved route](https://www.rystic.ai/docs/control-plane.md#status-codes) returns the simulator's own 404, and a 401 has its own envelope ([Auth](#auth)).

### Rate limits

[Network profiles](https://www.rystic.ai/docs/network-profiles.md) send a `429` you shape, with the body and headers you choose; the simulator counts no requests and never returns `429` on its own ([Limitations](#limitations)).

### Pagination

Not applicable: listing and search go through QuickBooks' query endpoint, which [Limitations](#limitations) lists as not served, so read each record by its `Id`.

### Idempotency

A `?requestid=<id>` replays a write only on the entities in the table's first row. On every other entity a retry writes again ([Limitations](#limitations)):

| Write, create or update | A retry with the same `requestid` |
|---|---|
| Item, Invoice, Estimate, CreditMemo, Deposit, VendorCredit, Term | returns the stored response |
| Customer, SalesReceipt | runs again, but stores its response for the entities above to replay |
| Vendor, Class and every other entity | runs again |

On the first row's entities, a `requestid` another entity stored replays that entity's response ([Limitations](#limitations)). A `requestid` longer than 50 characters returns 400 `ValidationFault` code `6000` on Customer, Vendor, Item, Class and Term writes.

A stale `SyncToken` on an update or an invoice void returns 400 `ValidationFault` code `5010`, so sending one update twice fails the second time unless an entity in the table's first row replays it.

A delete (`?operation=delete`) of a Bill, BillPayment, CreditMemo, Deposit, Invoice, JournalEntry, Payment, Purchase, PurchaseOrder, RefundReceipt or Transfer succeeds with a stale `SyncToken`, as QuickBooks does. An Estimate, SalesReceipt or VendorCredit delete with a stale `SyncToken` returns 400 `ValidationFault` code `5010` (unconfirmed).

## Coverage

The simulator serves customers, the list entities, the TaxCode and TaxRate reads, fourteen transactions, CompanyInfo and the report reads under `/v3/company/{realmId}/`, in JSON. The query endpoint, batch, change data capture, webhooks, XML and the transactions' PDF reads and email sends are out of scope.

On 2026-11-07, the simulator matched QuickBooks Online on 491 of the 538 tests that compare the two (91.3%), and 2 of them gave no verdict.

<!-- BEGIN generated: served-operations (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

<!-- coverage-claim -->
96 routes, counted from the simulator's route table.

| Resource | Operations |
|---|---|
| Account (7) | `POST /v3/company/{realmId}/account`; `GET /v3/company/{realmId}/account/{accountId}`; `GET /v3/company/{realmId}/reports/AccountList`; `GET /v3/company/{realmId}/reports/BalanceSheet`; `GET /v3/company/{realmId}/reports/CashFlow`; `GET /v3/company/{realmId}/reports/GeneralLedger`; `GET /v3/company/{realmId}/reports/TrialBalance` |
| Bill (6) | `POST /v3/company/{realmId}/bill`; `POST /v3/company/{realmId}/bill?operation=delete`; `GET /v3/company/{realmId}/bill/{billId}`; `GET /v3/company/{realmId}/reports/AgedPayableDetail`; `GET /v3/company/{realmId}/reports/AgedPayables`; `GET /v3/company/{realmId}/reports/TransactionListByVendor` |
| BillPayment (4) | `POST /v3/company/{realmId}/billpayment`; `POST /v3/company/{realmId}/billpayment?include=void&operation=update`; `POST /v3/company/{realmId}/billpayment?operation=delete`; `GET /v3/company/{realmId}/billpayment/{billpaymentId}` |
| Class (3) | `POST /v3/company/{realmId}/class`; `GET /v3/company/{realmId}/class/{classId}`; `GET /v3/company/{realmId}/reports/ClassSales` |
| Company info (8) | `GET /v3/company/{realmId}/companyinfo/{companyId}`; `GET /v3/company/{realmId}/reports/CustomerIncome`; `GET /v3/company/{realmId}/reports/DepartmentSales`; `GET /v3/company/{realmId}/reports/JournalReport`; `GET /v3/company/{realmId}/reports/ProfitAndLoss`; `GET /v3/company/{realmId}/reports/TransactionList`; `GET /v3/company/{realmId}/reports/TransactionListByCustomer`; `GET /v3/company/{realmId}/reports/TransactionListWithSplits` |
| CreditMemo (3) | `POST /v3/company/{realmId}/creditmemo`; `POST /v3/company/{realmId}/creditmemo?operation=delete`; `GET /v3/company/{realmId}/creditmemo/{creditmemoId}` |
| Customer (7) | `POST /v3/company/{realmId}/customer`; `GET /v3/company/{realmId}/customer/{customerId}`; `GET /v3/company/{realmId}/reports/AgedReceivableDetail`; `GET /v3/company/{realmId}/reports/AgedReceivables`; `GET /v3/company/{realmId}/reports/CustomerBalance`; `GET /v3/company/{realmId}/reports/CustomerBalanceDetail`; `GET /v3/company/{realmId}/reports/CustomerSales` |
| Department (2) | `POST /v3/company/{realmId}/department`; `GET /v3/company/{realmId}/department/{departmentId}` |
| Deposit (3) | `POST /v3/company/{realmId}/deposit`; `POST /v3/company/{realmId}/deposit?operation=delete`; `GET /v3/company/{realmId}/deposit/{depositId}` |
| Employee (2) | `POST /v3/company/{realmId}/employee`; `GET /v3/company/{realmId}/employee/{employeeId}` |
| Estimate (3) | `POST /v3/company/{realmId}/estimate`; `POST /v3/company/{realmId}/estimate?operation=delete`; `GET /v3/company/{realmId}/estimate/{estimateId}` |
| Invoice (5) | `POST /v3/company/{realmId}/invoice`; `POST /v3/company/{realmId}/invoice?operation=delete`; `POST /v3/company/{realmId}/invoice?operation=void`; `GET /v3/company/{realmId}/invoice/{invoiceId}`; `GET /v3/company/{realmId}/reports/ProfitAndLossDetail` |
| Item (5) | `POST /v3/company/{realmId}/item`; `GET /v3/company/{realmId}/item/{itemId}`; `GET /v3/company/{realmId}/reports/InventoryValuationDetail`; `GET /v3/company/{realmId}/reports/InventoryValuationSummary`; `GET /v3/company/{realmId}/reports/ItemSales` |
| JournalEntry (3) | `POST /v3/company/{realmId}/journalentry`; `POST /v3/company/{realmId}/journalentry?operation=delete`; `GET /v3/company/{realmId}/journalentry/{journalentryId}` |
| Payment (4) | `POST /v3/company/{realmId}/payment`; `POST /v3/company/{realmId}/payment?include=void&operation=update`; `POST /v3/company/{realmId}/payment?operation=delete`; `GET /v3/company/{realmId}/payment/{paymentId}` |
| Payment method (2) | `POST /v3/company/{realmId}/paymentmethod`; `GET /v3/company/{realmId}/paymentmethod/{paymentmethodId}` |
| Purchase (3) | `POST /v3/company/{realmId}/purchase`; `POST /v3/company/{realmId}/purchase?operation=delete`; `GET /v3/company/{realmId}/purchase/{purchaseId}` |
| PurchaseOrder (3) | `POST /v3/company/{realmId}/purchaseorder`; `POST /v3/company/{realmId}/purchaseorder?operation=delete`; `GET /v3/company/{realmId}/purchaseorder/{purchaseorderId}` |
| RefundReceipt (3) | `POST /v3/company/{realmId}/refundreceipt`; `POST /v3/company/{realmId}/refundreceipt?operation=delete`; `GET /v3/company/{realmId}/refundreceipt/{refundreceiptId}` |
| SalesReceipt (4) | `POST /v3/company/{realmId}/salesreceipt`; `POST /v3/company/{realmId}/salesreceipt?include=void&operation=update`; `POST /v3/company/{realmId}/salesreceipt?operation=delete`; `GET /v3/company/{realmId}/salesreceipt/{salesreceiptId}` |
| TaxAgency (1) | `GET /v3/company/{realmId}/reports/TaxSummary` |
| TaxCode (1) | `GET /v3/company/{realmId}/taxcode/{taxcodeId}` |
| TaxRate (1) | `GET /v3/company/{realmId}/taxrate/{taxrateId}` |
| Term (2) | `POST /v3/company/{realmId}/term`; `GET /v3/company/{realmId}/term/{termId}` |
| Transfer (3) | `POST /v3/company/{realmId}/transfer`; `POST /v3/company/{realmId}/transfer?operation=delete`; `GET /v3/company/{realmId}/transfer/{transferId}` |
| Vendor (5) | `GET /v3/company/{realmId}/reports/VendorBalance`; `GET /v3/company/{realmId}/reports/VendorBalanceDetail`; `GET /v3/company/{realmId}/reports/VendorExpenses`; `POST /v3/company/{realmId}/vendor`; `GET /v3/company/{realmId}/vendor/{vendorId}` |
| VendorCredit (3) | `POST /v3/company/{realmId}/vendorcredit`; `POST /v3/company/{realmId}/vendorcredit?operation=delete`; `GET /v3/company/{realmId}/vendorcredit/{vendorcreditId}` |

<!-- END generated: served-operations -->

<!-- BEGIN generated: feed-effects (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

**Feed effects.** No write emits a feed effect; watch requests instead. Each call appears on the event feed (`/_rystic/events`) as a `request` event, and reading the record back shows what it changed.

<!-- END generated: feed-effects -->

## Scenarios

<!-- BEGIN generated: scenarios (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

The simulator ships 5 scenarios; the quickstart boots `quickbooks_customer_ledger`.

| Scenario | What it is for | What it seeds | Clock |
|---|---|---|---|
| `quickbooks_customer_ledger` (default) | Blue Heron Bakery's eight customers | company_info 1, customer 8, request_dedup 2, taxagency 1, taxcode 1, taxrate 1 | 2026-01-19T00:00:00Z |
| `quickbooks_fresh_books` | A company file opened a week before the clock | company_info 1, customer 1, request_dedup 1, taxagency 1, taxcode 1, taxrate 1 | 2026-01-19T00:00:00Z |
| `quickbooks_inactive_archive` | A company of mostly inactive customers | company_info 1, customer 5, request_dedup 2, taxagency 1, taxcode 1, taxrate 1 | 2026-01-19T00:00:00Z |
| `quickbooks_job_hierarchy` | A contractor's customer tree | company_info 1, customer 5, request_dedup 1, taxagency 1, taxcode 1, taxrate 1 | 2026-01-19T00:00:00Z |
| `quickbooks_replay_desk` | Four stored customer requests | company_info 1, customer 3, request_dedup 4, taxagency 1, taxcode 1, taxrate 1 | 2026-01-19T00:00:00Z |

What each scenario holds:

- **`quickbooks_customer_ledger`**: Two paid up, five with open balances and one whose `BalanceWithJobs` is above its own `Balance`, so customer and CompanyInfo reads return records before any write.
- **`quickbooks_fresh_books`**: Settings configured, one customer with a zero balance and one stored `requestid`.
- **`quickbooks_inactive_archive`**: Four of five deactivated, one of them with an open balance.
- **`quickbooks_job_hierarchy`**: A parent with two jobs, one a Project and one with a job of its own two levels deep, plus a flat customer, so `ParentRef`, `Level` and `BillWithParent` reads have targets.
- **`quickbooks_replay_desk`**: A create, an update, a deactivation and a validation fault, against three customers. A customer write stores its `requestid` but never replays it.

Boot one by name, or load one into a running simulator:

```bash
rystic run quickbooks-twin -d --port 0 --scenario quickbooks_customer_ledger
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"quickbooks_customer_ledger"}'
```

<!-- END generated: scenarios -->

### What the scenarios do not hold

- No list entity or transaction: each scenario seeds one company and its customers, so create an account, an item or a vendor with the API before a transaction references it ([Transactions and list entities](https://www.rystic.ai/docs/simulators/quickbooks/features/transactions.md)).
- One tax code, one tax rate and one tax agency only. Every scenario's customers name tax code `2` in `DefaultTaxCodeRef`, that code lists rate `1`, and the rate names agency `1`, the only `agency_id` TaxSummary accepts. Seed more in `state_taxcode`, `state_taxrate` and `state_taxagency` ([Seeding state](#seeding-state)).
- Without `--scenario`, the simulator boots with no records, and the CompanyInfo read returns 403 `AuthorizationFault`.

## Seeding state

Each entity is one register named `state_<entity>`, such as `state_customer`, `state_vendor` or `state_invoice`. A register keys its records by `Id`, except `state_company_info`, which is keyed by realm id. `state_request_dedup` keys each stored response by its `requestid`, as `{"requestid": "…", "response": {…}}`, and a replay returns `response` as its body. Seed a company under its realm id, with `realmId` in the record too, and a customer, then read them back:

```bash
curl -s -X POST "$RYSTIC_URL/_rystic/seed" -d '{"state_company_info":{"9341452000000001":{"realmId":"9341452000000001","Id":"1","CompanyName":"Example Co"}},"state_customer":{"100":{"Id":"100","DisplayName":"Seeded Customer","SyncToken":"0","Active":true}}}'
curl -s "$RYSTIC_URL/v3/company/9341452000000001/companyinfo/9341452000000001" -H 'Authorization: Bearer placeholder' | jq -c '.CompanyInfo | {Id, CompanyName, LegalName, NameValue}'
# -> {"Id":"1","CompanyName":"Example Co","LegalName":"Company 9341452000000001","NameValue":[]}
curl -s "$RYSTIC_URL/v3/company/9341452000000002/companyinfo/9341452000000002" -H 'Authorization: Bearer placeholder' | jq -c '.Fault | {type, code: .Error[0].code}'
# -> {"type":"AuthorizationFault","code":"3100"}
curl -s "$RYSTIC_URL/v3/company/9341452000000001/customer/100" -H 'Authorization: Bearer placeholder' | jq -c '.Customer | {Id, DisplayName, SyncToken}'
# -> {"Id":"100","DisplayName":"Seeded Customer","SyncToken":"0"}
```

| When you read `GET …/companyinfo/{companyId}` and | It returns |
|---|---|
| a company is seeded under the path's realm | that company |
| none is | 403 `AuthorizationFault`, as for a realm the token is not issued for |
| either path id is not numeric | 404 with an empty body |

`{companyId}` selects nothing. A field the seeded company leaves out is filled in: `CompanyName` and `LegalName` become `Company <realmId>`, and `NameValue` is empty. [Registers](https://www.rystic.ai/docs/concepts/worlds.md#registers) lists each register's fields, and [Scenarios and seeding](https://www.rystic.ai/docs/concepts/worlds.md) covers the seed response, refusals, boot files and the seed gate.

## Features

<!-- BEGIN generated: features (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

Each feature has its own page:

- [Customers](https://www.rystic.ai/docs/simulators/quickbooks/features/customers.md): The customer lifecycle QuickBooks applies: create, read, sparse and full update, deactivate and reactivate, with SyncToken checks, name rules and stored sub-customers.
- [Transactions and list entities](https://www.rystic.ai/docs/simulators/quickbooks/features/transactions.md): Vendors, items, accounts and the other list entities, and fourteen transactions from Invoice to Transfer, with delete, void and the amounts each keeps.

<!-- END generated: features -->

## Determinism and reset

Each entity numbers its records from `1`, except Class, which starts at `5000000000000007280`. The `V4IDPseudonym` sequence starts at `000000000000000000000001`. The clock moves one millisecond for each request that renders a time, reads included.

The simulator renders the times it writes at a fixed offset:

| Entity | Offset |
|---|---|
| Bill, Payment, Deposit, VendorCredit, CreditMemo and Transfer | `-08:00` |
| a voided SalesReceipt's `LastUpdatedTime` and `time` | `-08:00` |
| most other entities | `-07:00` |

It writes `MetaData.CreateTime` and `MetaData.LastUpdatedTime` to the second. A customer response's `time` is to the second too, and a CompanyInfo response's `time` to the millisecond. A 401's `time` format depends on the route; see [Auth](#auth).

[Reset and determinism](https://www.rystic.ai/docs/concepts/reset-and-determinism.md) covers reset, the clock and the RNG.

## Limitations

<!-- BEGIN generated: known-gaps (go run ./ingest/cmd/twindocs quickbooks; do not edit) -->

**Known issues.**

- A `requestid` is shared across entities: a write replays whatever another entity stored under the same id, where QuickBooks scopes it to the request.
- No request rate is enforced, where QuickBooks throttles each realm's requests with 429.
- A retried `requestid` writes again on every entity but Item, Invoice, Estimate, CreditMemo, Deposit, VendorCredit and Term, where QuickBooks returns the stored response.

<!-- gaps-claim -->
Beyond the known issues, 57 of the 153 operations in the published spec are not served, 3 success responses may leave out fields the spec requires, 470 documented responses have no test yet and 325 behaviors are not modeled. The details follow.

**Not served.** 57 of the 153 operations in the published spec are not served.

- CompanyInfo, 1 of 2 served: `POST /v3/company/{realmId}/companyinfo`
- CreditMemo, 3 of 5 served: `GET /v3/company/{realmId}/creditmemo/{creditmemoId}/pdf`, `POST /v3/company/{realmId}/creditmemo/{creditmemoId}/send`
- Estimate, 3 of 5 served: `GET /v3/company/{realmId}/estimate/{estimateId}/pdf`, `POST /v3/company/{realmId}/estimate/{estimateId}/send`
- Invoice, 4 of 6 served: `GET /v3/company/{realmId}/invoice/{invoiceId}/pdf`, `POST /v3/company/{realmId}/invoice/{invoiceId}/send`
- Payment, 4 of 6 served: `GET /v3/company/{realmId}/payment/{paymentId}/pdf`, `POST /v3/company/{realmId}/payment/{paymentId}/send`
- PurchaseOrder, 3 of 5 served: `GET /v3/company/{realmId}/purchaseorder/{purchaseorderId}/pdf`, `POST /v3/company/{realmId}/purchaseorder/{purchaseorderId}/send`
- RefundReceipt, 3 of 5 served: `GET /v3/company/{realmId}/refundreceipt/{refundreceiptId}/pdf`, `POST /v3/company/{realmId}/refundreceipt/{refundreceiptId}/send`
- SalesReceipt, 4 of 6 served: `GET /v3/company/{realmId}/salesreceipt/{salesreceiptId}/pdf`, `POST /v3/company/{realmId}/salesreceipt/{salesreceiptId}/send`
- Not served at all: Attachable (5), Budget (3), CDCResponse (1), ChangeOrder (3), CompanyCurrency (2), CreditCardPaymentTxn (3), CustomerType (1), ExchangeRate (2), IntuitBatchRequest (1), InventoryAdjustment (3), Preferences (2), QueryResponse (2), RecurringTransaction (3), ReimburseCharge (1), TaxAgency (2), TaxClassification (3), TaxPayment (1), TaxService (1), TimeActivity (3)

**Missing response fields.** The spec requires these fields in a success response, and the simulator may leave them out:

- `GET /v3/company/{realmId}/transfer/{transferId}` (200): `Transfer/TransferEx`
- `POST /v3/company/{realmId}/transfer` (200): `Transfer/TransferEx`
- `POST /v3/company/{realmId}/transfer?operation=delete` (200): `Transfer/TransferEx`

**Not tested.** 549 tests compare the simulator with the real service. 470 documented responses have no test yet, and 141 of them need a credential our test account doesn't hold (401 ×45, 403 ×96). The other 329:

| Operation | Status | Why |
|---|---:|---|
| `GET /v3/company/{realmId}/account/{accountId}` | 400 | a well-formed request does not provoke it |
| `GET /v3/company/{realmId}/account/{accountId}` | 429 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/account/{accountId}` | 500 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/account/{accountId}` | 503 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/bill/{billId}` | 400 | a well-formed request does not provoke it |
| `GET /v3/company/{realmId}/bill/{billId}` | 429 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/bill/{billId}` | 500 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/bill/{billId}` | 503 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/billpayment/{billpaymentId}` | 400 | a well-formed request does not provoke it |
| `GET /v3/company/{realmId}/billpayment/{billpaymentId}` | 429 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/billpayment/{billpaymentId}` | 500 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/billpayment/{billpaymentId}` | 503 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/class/{classId}` | 400 | a well-formed request does not provoke it |
| `GET /v3/company/{realmId}/class/{classId}` | 429 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/class/{classId}` | 500 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/class/{classId}` | 503 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/companyinfo/{companyId}` | 429 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/companyinfo/{companyId}` | 500 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/companyinfo/{companyId}` | 503 | no request a client sends provokes it |
| `GET /v3/company/{realmId}/creditmemo/{creditmemoId}` | 400 | a well-formed request does not provoke it |
| … and 301 more | | |

**Not modeled.** Behavior the simulator does not model, by operation:

- The 404 and 429 bodies are undocumented by Intuit, so only their statuses are modeled. (`GET /v3/company/{realmId}/billpayment/{billpaymentId}`)
- The 404, 429, 500 and 503 bodies are undocumented, so only their statuses are claimed. (`GET /v3/company/{realmId}/employee/{employeeId}`)
- The request shape that provokes the documented 404: the operation names no resource by a path or query identifier whose absence is documented to produce it. (`GET /v3/company/{realmId}/reports/AgedReceivableDetail`)
- Whether `start_duedate` greater than `end_duedate` is a 400 or an empty report. (`GET /v3/company/{realmId}/reports/AgedReceivableDetail`)
- Whether a customer or term ID in the filter that names no record is refused (610) or simply matches nothing. (`GET /v3/company/{realmId}/reports/AgedReceivableDetail`)
- Whether an unknown column name in `columns` is refused or silently dropped. (`GET /v3/company/{realmId}/reports/AgedReceivableDetail`)
- Whether qzurl=true adds a href to each ColData of this report, and the shape of that URL, is undocumented beyond the ColData schema. (`GET /v3/company/{realmId}/reports/AgedReceivables`)
- 500 InternalServerError (SystemFault code 10000) and 503 ServiceUnavailable: server-side faults with no caller-side trigger documented. (`GET /v3/company/{realmId}/reports/ClassSales`)
- The report's exact row and column layout under each summarize_column_by grouping, and the arpaid/date_macro effect on which transactions are counted: only the default Total grouping sample is documented. (`GET /v3/company/{realmId}/reports/CustomerBalance`)
- The report's own content rules are undocumented here: which transactions a customer section lists, how arpaid Paid/All change the rows, how aging_method Current shifts the aging, and which column types a given columns value adds. (`GET /v3/company/{realmId}/reports/CustomerBalanceDetail`)
- Whether a customer, term or department ID naming no record is refused (610 Object Not Found) or silently filters to an empty report is undocumented. (`GET /v3/company/{realmId}/reports/CustomerBalanceDetail`)
- The exact row and column grammar under summarize_column_by values other than the default Total; only the default-grouped sample is documented. (`GET /v3/company/{realmId}/reports/CustomerIncome`)
- The exact column set and row grouping each summarize_column_by value produces; only the Total form is exemplified. (`GET /v3/company/{realmId}/reports/CustomerSales`)
- What provokes the documented 404 on this route: it takes no resource path parameter beyond realmId, which the run supplies, so no in-scope request shape names a resource that can be made absent. (`GET /v3/company/{realmId}/reports/CustomerSales`)
- Whether qzurl=true adds ColData.href on every data row or only on some column types. (`GET /v3/company/{realmId}/reports/CustomerSales`)
- The documented hierarchy defect ('the response for the General Ledger report hierarchy is broken in certain circumstances when there are sub accounts configured'): the conditions and the resulting shape are not stated, so the Rows tree is modeled only as the documented nesting. (`GET /v3/company/{realmId}/reports/GeneralLedger`)
- No body is documented for 404, 429, 500 or 503 on this route. (`GET /v3/company/{realmId}/reports/InventoryValuationSummary`)
- Whether the Header carries Item and SummarizeColumnsBy when those parameters are supplied is undocumented here; the sample shows neither. (`GET /v3/company/{realmId}/reports/InventoryValuationSummary`)
- The Header's NoReportData option semantics beyond the sample value 'false'. (`GET /v3/company/{realmId}/reports/ItemSales`)
- The exact ordering ascend/descend imposes on the Rows array. (`GET /v3/company/{realmId}/reports/ItemSales`)
- … and 305 more

<!-- END generated: known-gaps -->

**By design.**

- **No realm isolation.** A read selects by `Id` without checking the realm in its path. Creating a record under a realm id that is not all digits returns 404 with an empty body, and so do the customer, CompanyInfo, Bill, PaymentMethod, PurchaseOrder and SalesReceipt reads. Every other read serves any realm id. Voiding a BillPayment returns that 404 under a realm id of fewer than 10 digits or of one repeated digit.

  **Differs from QuickBooks:** QuickBooks scopes every record to its realm.
- **Bodies are read as JSON whatever their `Content-Type`.** An XML body returns the unparsable-body refusal ([Error format](#error-format)).

Release notes: [Changelog](https://www.rystic.ai/docs/simulators/quickbooks/changelog.md)

---

Source: https://www.rystic.ai/docs/simulators/quickbooks · Markdown: https://www.rystic.ai/docs/simulators/quickbooks.md
