> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qubesync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test without QuickBooks

> Simulated connections answer like QuickBooks Desktop within seconds, and fail on demand, so you can build and test every path of your integration before touching a real company file.

Testing a QuickBooks Desktop integration has usually meant a Windows VM, a licensed copy of QuickBooks, and
waiting minutes for the Web Connector on every change. Worse, the failures that matter most, such as a
duplicate name, QuickBooks closed or a stale `edit_sequence`, are hard to reproduce on purpose.

A **simulated connection** replaces all of that. It's a fake QuickBooks Desktop company file hosted by QuBe
Sync, driven by the same Web Connector session code as a real connection, so your requests, webhooks and
workflows go through the same code they will in production. It answers within seconds, and you can make it fail
however you like.

> [!NOTE]
>
> Simulated connections are in beta. The simulator doesn't match real QuickBooks in every case yet, and its
> answers change as it gets closer: a change that makes it more accurate isn't treated as a breaking change.
> The simulator's endpoints, its fault names and the `simulated` connection type are stable. A request it
> doesn't model is answered with status 1030 rather than a guess, and the gaps we know about are listed
> below. [Test against real QuickBooks](#then-test-against-the-real-thing) before you launch, and if an
> answer differs from what real QuickBooks says, tell us at [support@qubesync.com](mailto:support@qubesync.com).

## Create one

Request

<pre>`POST 
          /api/v2`</pre>

```http theme={null}
POST /api/v2/connections

{
  "name": "Test company",
  "type": "simulated"
}
```

```bash theme={null}
curl -u "$QUBE_API_KEY:" \
  -X POST \
  "https://qubesync.com/api/v2/connections" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test company",
    "type": "simulated"
  }'
```

It's connected as soon as it exists. There's nothing to install and no onboarding. From the CLI:
`qube connections create --simulated --name "Test company"`. Simulated connections are never billed.

## What's in the company file

Every simulated company starts with the same small, realistic data set, so your tests can rely on it:

| Kind | Records |
| - | - |
| Customers | Riverbend Electric, Hillcrest Dental, Marisol Vega |
| Vendors | Lone Star Supply Co, Capital Freight |
| Items | Consulting (\$150, service), Installation (\$95, service), Shipping (\$25, non-inventory), Widget (\$40, 120 on hand), Gadget (\$129, 35 on hand), Rush Fee (\$50, other charge) |
| Accounts | Checking, Accounts Receivable, Inventory Asset, Undeposited Funds, Accounts Payable, Sales Tax Payable, Opening Balance Equity, Sales, Cost of Goods Sold, Office Supplies |
| Other lists | Terms (Net 15, Net 30, Due on receipt), sales tax codes (Tax, Non), payment methods (Check, Cash, Visa), ship methods, customer types, classes |

Customers, vendors, employees, items and transactions such as invoices, bills and payments can be queried,
added and modified. The simulator enforces QuickBooks' rules along the way: names are unique across lists
(3100), references must exist (3140), an amount needs its two decimals (`40.00`, not `40`; 3040), and
updates need the current `edit_sequence` (3200). As on real QuickBooks, a 3200's qbXML answer carries
the record as it stands, current `edit_sequence` included, though the JSON answer passes on only the
error. Values keep
their spacing as QuickBooks keeps it: a name's double space or a note's line break comes back exactly as
sent, and a `full_name` filter finds a record by that spelling, or by one with extra spaces, but a
single-spaced filter does not find a double-spaced name. Iterated queries page like the real thing.
The company, host and preferences queries answer from the company file too: its preferences are a new
file's, with account numbers, class tracking, inventory and estimates on, one currency and sales tax off,
and they can't be changed. Requests it doesn't model yet, such as reports, are answered with status 1030,
"not supported by this implementation".

Reset it to the starting data at any time:

Request

<pre>`POST 
          /api/v2`</pre>

```http theme={null}
POST /api/v2/connections/{connection_id}/simulator/reset
```

```bash theme={null}
curl -u "$QUBE_API_KEY:" \
  -X POST \
  "https://qubesync.com/api/v2/connections/$CONN_ID/simulator/reset"
```

## Transactions and their lines

Transactions keep their lines the way QuickBooks keeps them, so you can build and read line items
against the simulator:

| Transactions | Lines they keep |
| - | - |
| **Sales forms**: invoices, estimates, sales orders, sales receipts, credit memos | Item and item-group lines |
| **Purchases**: bills, checks, credit card charges and credits, item receipts, vendor credits | `item_line_add`, `item_group_line_add` and `expense_line_add` lines |

Every line gets a `txn_line_id`. A line that names an item, account, customer or item group that doesn't
exist is refused with 3140. Queries return item, group and expense lines only with `include_line_items`
(a journal entry's lines come back either way). Lines come back in the order QuickBooks received them.

> [accordion] Prices, amounts and totals
>
> An item line with no price starts from the item's: its sales price on a sale, its purchase cost on a
> purchase. A `quantity` and an `amount` without a price imply one. Otherwise `amount` is `quantity`
> times the price, with a quantity of 1 when none is given. An expense line carries the `amount` you
> send. An item-group line expands into one line per item in the group, each item's quantity times the
> group's.
>
> Totals come from the lines:
>
> | Transaction | Totals |
> | - | - |
> | Invoice | `subtotal` and `balance_remaining` |
> | Bill | `amount_due` and `open_amount` |
> | Check or card charge | `amount` |
> | Item receipt | `total_amount` |
> | Vendor credit | `credit_amount` and `open_amount` |
>
> A bill posts to Accounts Payable and falls due by its terms, or 10 days after its date.

> [accordion] Updating lines
>
> Updating lines follows QuickBooks' rule:
>
> | To | Do this |
> | - | - |
> | Add a line | Send `txn_line_id` `-1` |
> | Change a line | Send its existing `txn_line_id` |
> | Remove a line | Leave it out |
> | Empty a list | Send `clear_item_lines` or `clear_expense_lines` |
> | Leave the lines alone | Send no lines |
>
> Item and group lines count as one list. A purchase's expense lines are a separate list, replaced only
> when you send `expense_line_mod`.

> [accordion] Journal entries
>
> Journal entries keep their debit and credit lines. `journal_line_mod` changes them by `txn_line_id`
> under the same rule as other lines, and `journal_line_type` (`Debit` or `Credit`) moves a line to the
> other side. A new line needs a type.
>
> An entry whose debits and credits don't balance is refused with 3180 when it's created and 3170 when
> it's updated, with the message "Transaction not in balance." Both codes and full messages match what
> real QuickBooks answers. An entry created with an entry-level `memo` is saved without it and answered
> with a status 530 warning (`The field "Memo" is not supported by this implementation.`), as real
> QuickBooks does; a `memo` on a line is kept.

> [accordion] Lines inside a group
>
> Lines inside a group can be updated too: an `invoice_line_group_mod` (or a purchase's
> `item_group_line_mod`) with its own line updates changes the group's lines by `txn_line_id` under the
> same rule, and a new group `quantity` scales its lines. Intuit doesn't document how real QuickBooks
> handles these updates. The simulator copies how a group behaves when it's added, so test group
> updates against real QuickBooks too.

## Payments, deposits, voids and custom fields

> [accordion] Payments
>
> Payments pay invoices down: an `applied_to_txn_add`'s `payment_amount` lowers the invoice's
> `balance_remaining` and marks it paid at zero. A `set_credit` applies a credit memo first: its
> `applied_amount` comes off the invoice's `balance_remaining` and the credit memo's `credit_remaining`.
> An `invoice.create` can apply credit memos with `set_credit` the same way.
>
> The part of a payment the invoice no longer needs is its `unused_payment`: a `total_amount` above the
> `payment_amount`s keeps the rest, and `is_auto_apply: false` keeps all of it, applied to nothing. As on
> real QuickBooks, a payment's `unused_credits` is all the credit its customer has when you read it: what
> its credit memos have left, plus the `unused_payment` of its other payments (never this one's), whether
> or not this payment applied any, so it falls to 0 once that credit is applied elsewhere. Each
> `applied_to_txn_ret` gives the invoice's `balance_remaining` and `txn_date` as they are when you read
> the payment, not when it was made: after an `invoice.update` raises the total, a payment's
> `balance_remaining` rises with it. Its `amount` is the payment's part only.
>
> A `payment_amount` more than the invoice's `balance_remaining` is refused with 3210 ("You cannot pay
> more than the amount due.") and nothing is saved, as on real QuickBooks. The simulator caps a credit
> at what's available and what the invoice owes, and a payment that the line's own credits leave more
> than the invoice needs at what's left; real QuickBooks may refuse those too. A payment sent without a
> `ref_number` has none, as on real QuickBooks, which doesn't number payments the way it numbers
> invoices.

> [accordion] Deleting a payment
>
> Deleting a payment (`txn.delete`) gives each invoice it paid its `balance_remaining` and
> `applied_amount` back and takes it out of the invoice's `linked_txn`. As on real QuickBooks, the
> invoice's `time_modified` moves to the deletion but its `edit_sequence` doesn't. Credits the payment's
> own `set_credit` applied stay applied.

> [accordion] Linked transactions
>
> The simulator lists linked transactions as real QuickBooks lists them:
>
> | `linked_txn` | Lists |
> | - | - |
> | In each of a payment's `applied_to_txn_ret` | Everything else applied to that invoice, in the order it was applied. That means credit memos, applied before or after this payment (by its own `set_credit` or a payment of credits alone too), and the invoice's other payments, but never the payment itself. |
> | On the invoice: in the answer to `invoice.update`, and from `invoice.query` only with `include_linked_txns` | Everything applied to it, a payment's own credits before the payment. |
> | On a credit memo, from `credit_memo.query` with `include_linked_txns` | The invoice its credit went to (`txn_type` `Invoice`, the credit as a negative `amount`), not the payment that applied it. |
>
> Under a payment, each gives the other transaction's `txn_id`, `txn_type` (`CreditMemo` or
> `ReceivePayment`), `txn_date`, `ref_number`, `link_type` `AMTTYPE`, and what it took off the invoice
> as a negative `amount`.
>
> Every list shows what's applied when you read it, with each transaction's current `txn_date`, so a
> credit applied after a payment shows up under that payment. For the total applied to an invoice, read
> its `applied_amount`, since a payment's `linked_txn` leaves out the payment's own part.

> [accordion] What a payment does to the invoice
>
> As real QuickBooks does it, the invoice's `applied_amount` is negative (an invoice paid 80.00 reads
> `-80.00`), and its `balance_remaining` is its total plus `applied_amount`. The invoice's
> `time_modified` moves to the payment's time, but its `edit_sequence` doesn't, so the `edit_sequence`
> you last read still updates it. A credit memo whose credit is applied keeps its `edit_sequence` the
> same way.
>
> In the other direction, an `invoice.update` that changes a line's `amount` (even with the total
> unchanged) or the invoice's `txn_date` moves the `time_modified` of every payment and credit memo
> applied to it, and leaves their `edit_sequence`, so a sync by `modified_date_range_filter` picks them
> up again. One that changes only the `memo` or a line's `desc` leaves them alone.

> [accordion] Reading payments back
>
> `receive_payment.query` returns what each payment was applied to (`applied_to_txn_ret`) only with
> `include_line_items`, as real QuickBooks does. Without it, a payment that paid invoices comes back
> without them. The answer to `receive_payment.create` always has them.

> [accordion] A payment of credits alone
>
> A payment of credits alone (`total_amount` `0.00`, every `applied_to_txn_add` a `set_credit` with no
> `payment_amount`) applies the credits but creates no payment, as in real QuickBooks. Its answer has no
> `txn_id` of its own: `applied_to_txn_ret` lists only each invoice's `txn_id` and `txn_type`. Read the
> invoice or credit memo again to see what was applied, and don't store an id for that payment. The
> credit then shows as a `linked_txn` on the invoice and under each of its payments.

> [accordion] Deposits
>
> A payment received without a `deposit_to_account_ref` goes to Undeposited Funds, as in QuickBooks.
> `receive_payment_to_deposit.query` lists it until a deposit takes it. A `deposit.create` line with a
> `payment_txn_id` deposits that payment for its whole amount, and a line with an `account_ref` and
> `amount` deposits money from that account. `deposit_total` is the sum of the lines.
>
> As in real QuickBooks, each payment a deposit takes gets the deposit's time as its `time_modified` and
> keeps its `edit_sequence`. A payment that isn't waiting to be deposited (unknown, deposited already, or
> received straight into a bank account) is refused with 3120. Deleting a deposit puts its payments back
> in Undeposited Funds.

> [accordion] Voids
>
> `txn_void.execute` keeps the transaction and sets its totals and every line's amount and quantity to
> 0, for bills, checks, credit card charges and credits, credit memos, invoices, item receipts, journal
> entries, sales receipts and vendor credits. Each line keeps its rate, the memo gets `VOID: ` in front,
> and a voided invoice reads back `is_paid` true, even one nothing paid. It keeps its `ref_number`.
>
> An invoice that payments paid is voided too, as real QuickBooks does: each payment keeps its
> `total_amount`, loses its `applied_to_txn_ret` for that invoice, and what it had applied there is added
> to its `unused_payment`, ready to apply to another invoice. Real QuickBooks does all this for an
> invoice; for the other types it's the simulator's reading. An unknown `txn_id`, or one of another
> type, gets 3120.

> [accordion] Custom fields and private data extensions
>
> `data_ext_def.create`, `.update`, `.delete` and `.query` keep definitions per company, unique by
> `owner_id` and `data_ext_name`. Following Intuit's rules:
>
> | | Custom field | Private extension |
> | - | - | - |
> | `owner_id` | `0` | A GUID |
> | Assigned to | Customer, Employee, Vendor or Item, and must be `STR255TYPE` | Any list or transaction type |
> | Can be deleted | No | Yes |
>
> A custom field assigned to Customer also reaches credit memos, estimates, invoices, sales orders and
> sales receipts. To get one onto sales orders, assign it to Customer, or use a private extension
> assigned to SalesOrder; the refusal says so.
>
> `data_ext.create`, `.update` and `.delete` set and clear a value on a list object or a transaction
> whose type the definition is assigned to. `data_ext.update` also sets a first value, as it does on
> real QuickBooks, so you don't need `data_ext.create` before it. A query that sends `owner_id` gets the
> values back as `data_ext_ret`, for those owners only.
>
> > [!WARNING]
> >
> > **Pass `owner_id` to `data_ext_def.query` too** (0 for custom fields, your GUID for private
> > extensions). Without it QuickBooks answers status 1 "did not find a matching object" even when
> > definitions exist, so an empty answer there doesn't prove a field is missing. The simulator answers
> > the same way.
>
> A custom field's definition comes back as real QuickBooks returns it: `data_ext_id` 0,
> `data_ext_format_string` "Any text", and `assign_to_object` listing the transactions a Customer field
> reaches. Refusals of what Intuit rules out (a custom field on a sales order, a value with no
> definition) use 3180, 3170, 3100 and 3120; Intuit doesn't document those codes, so treat them as
> approximate. An unknown `txn_id` gets 3120, as real QuickBooks answers it.

The simulator doesn't model sales tax (always 0), payments against bills (bills stay open), inventory
quantities or account balances moving, units of measure, linked transactions other than payments and
credits applied to an invoice (`link_to_txn_id`, or a bill's `linked_txn`),
price levels, account types (a card charge can name any account), date-driven terms, payment discounts, `is_auto_apply` (the payment is received
unapplied), changing what an update to an invoice or payment applies, sales receipts waiting in
Undeposited Funds, data extension values' types and required flags, or QuickBooks refusing a
transaction with no lines. It answers 1030 rather than guess for: voiding a deposit, bill payment,
statement charge, credit card refund or inventory adjustment, a transaction that's already void, an
invoice with a credit memo applied to it, or a credit memo that has been applied; a data extension on
a transaction line or on the company; and a deposit with cash back.

Other differences we know of:

| When | The simulator |
| - | - |
| A reference names a `list_id` that doesn't exist | Answers 3140. Real QuickBooks answers 3120 for an id, and 3140 only for a `full_name`. |
| You delete a record that's in use, such as a customer with invoices | Lets the delete succeed. |
| You query for deleted transactions (`txn_deleted.query`) | Always finds nothing. Deleted list records are listed by `list_deleted.query` for 90 days, as QuickBooks lists them. |
| You merge two list records (`list_merge.execute`) | Answers 1030: merges aren't simulated. |
| A payment is applied to a `txn_id` that doesn't exist, or a `set_credit` names a `credit_txn_id` that isn't a credit memo | Skips it without an error. |
| A request needs a feature, such as bar codes, currencies, inventory sites or units of measure | Allows it: every simulated company allows every feature. A real company file can answer 3250 ("This feature is not enabled or not available in this version of QuickBooks") for those, and decline payroll and sales-tax-return queries. |

## Make it fail

`PUT /connections/{id}/simulator` sets **faults**. Each `PUT` replaces the whole set, so send every fault
you want armed at once. Resetting clears them all.

Request

<pre>`PUT 
          /api/v2`</pre>

```http theme={null}
PUT /api/v2/connections/{connection_id}/simulator

{"faults": {"connection_error": "modal_dialog"}}
```

```bash theme={null}
curl -u "$QUBE_API_KEY:" \
  -X PUT \
  "https://qubesync.com/api/v2/connections/$CONN_ID/simulator" \
  -H "Content-Type: application/json" \
  -d '{"faults": {"connection_error": "modal_dialog"}}'
```

| Fault | Values | Behaves like | Lasts |
| - | - | - | - |
| `connection_error` | `qb_closed`, `company_file_missing`, `modal_dialog`, `company_file_mismatch`, `unexpected` | The Web Connector failing to reach QuickBooks right after it signs in, before it asks for any request: QuickBooks closed for an app allowed to run only while it's open, the company file missing so QuickBooks can't be started, a dialog open, the wrong company file open, or an unexplained failure. Requests go `retryable`, never sent. | Until a `PUT` leaves it out |
| `next_status` | `{"code": 3100}`, or any QuickBooks status code, with an optional `message` | QuickBooks refusing the next request with that code. | One request |
| `next_response_error` | `"xml_error"`, or `{"hresult", "message"}` | QuickBooks failing to parse the next request. With your own `message`, the Web Connector reporting an error in place of the answer: a connection error's message leaves the request `retryable`, and any other ends a create or update [`outcome_unknown`](https://docs.qubesync.com/error_handling#when-the-outcome-is-unknown) and a query `unexpected_error`. | One request |
| `next_lost_response` | `true` | QuickBooks applying the next request that changes the company file (an add, change, delete, void or merge), then the Web Connector dying before it answers. Queries and reports before it are answered normally and leave the fault armed, since a lost query is simply asked again. The request ends `timed_out`; a workflow that sent it [settles it](https://docs.qubesync.com/workflows.md#lost-answers). | The next write |
| `next_response_too_large` | `true` | QuickBooks applying the next request, then answering it with more than the [size limit](https://docs.qubesync.com/error_handling.md#when-the-answer-is-too-large) for one answer. The request ends `error` with `error_type` `response_too_large`; a workflow that sent it sees `no_response` with reason `too_large`. Sandbox companies are too small to reach the limit otherwise. | One request |
| `latency_ms` | `0` to `60000` | QuickBooks taking its time over each answer. | Until a `PUT` leaves it out |
| `stalled` | `not_checking_in`, `quickbooks_unavailable` | The QuickBooks computer switched off, or QuickBooks not open: the Web Connector doesn't run, so your requests wait, and once one is waiting you get [`connection.stalled`](https://docs.qubesync.com/webhooks#connection-events) with this `stall_reason` within seconds. Leaving it out sends `connection.resumed`, and the waiting requests are answered. | Until a `PUT` leaves it out |

`GET /connections/{id}/simulator` shows the current faults and a summary of the company file, and
`POST /connections/{id}/simulator/sync` runs the fake Web Connector right away, like clicking **Update
Selected**.

> [tab] CLI
>
> ```bash theme={null}
> qube simulator faults <connection> --next 3100      # the next request: name already in use
> qube simulator faults <connection> --next lost      # the next add, change or delete: applied, then its answer is lost
> qube simulator faults <connection> --qb closed      # QuickBooks closed until you say otherwise
> qube simulator faults <connection> --qb ok          # QuickBooks is back
> qube simulator faults <connection> --latency 5000   # five seconds per answer
> qube simulator faults <connection> --stall not_checking_in   # stalled until --stall off
> qube simulator sync <connection>                    # run the Web Connector now
> qube simulator reset <connection>
> ```
>
> `--next lost` arms `next_lost_response`, and `--next ok` clears whichever next-request fault is armed.
> `--stall` takes `not_checking_in` or `quickbooks_unavailable`, and `--stall off` clears it.

> [tab] Dashboard
>
> A simulated connection's page has a **Simulator** panel: choose whether QuickBooks is reachable, what
> happens to the **Next request** (a lost answer, which waits for the next add, change or delete, or 3100, 3120, 3140, 3180, 3200 or malformed XML), how
> long QuickBooks thinks (up to 15 seconds) and whether the connection is stalled. Run the Web Connector now, or reset the company file, and
> watch each request get queued and answered live. Other codes and latencies are set through the API.

### Test your stall handling

A real connection stalls when your requests wait past a grace of at least 15 minutes, so the
`stalled` fault is how you try your [`connection.stalled` and `connection.resumed`](https://docs.qubesync.com/webhooks#connection-events)
handler without one. Give the connection a `webhook_url`, then:

1. Set `{"faults": {"stalled": "not_checking_in"}}`, or `quickbooks_unavailable`, the `stall_reason` you
   want to see.
2. Queue a request. It waits, as it would on a computer that's switched off, and within seconds you get
   `connection.stalled` with that `stall_reason`. If a request is already waiting when you set the fault,
   the event comes right away.
3. Optionally, set the other reason: `connection.stalled` comes again, with the new reason and a newer
   `revision`, as when a computer is turned on before QuickBooks is open.
4. Clear the fault (a `PUT` without it, or a reset). You get `connection.resumed`, and the waiting
   requests are answered.

The events are the ones a real stall sends, with the same fields. As on a real connection, nothing is
sent while nothing of yours is waiting, and `connection.resumed` also comes, within a few minutes, if you
delete the waiting requests. With the fault set, `POST /connections/{id}/simulator/sync` runs nothing and answers
`"result": "stalled"`.

## A test plan for your integration

Walk every path before your customers do:

| Scenario | Set up | Your app should |
| - | - | - |
| Happy path | nothing | Store the returned `list_id` / `txn_id` |
| Duplicate name | `next_status: {code: 3100}` | Ask the user, or use [`ensure`](https://docs.qubesync.com/workflows#pick-a-template) |
| Record changed in QuickBooks | `next_status: {code: 3200}` | Re-query, then retry the update. (A workflow's update reads the record back, finds it unchanged and sends it again.) |
| Answer lost | `next_lost_response: true`, then send a create or update | Query before resending a create or update. A workflow [settles it](https://docs.qubesync.com/workflows.md#lost-answers) itself |
| Record changed before a workflow's update | A chart of your own with an update state: change the record through the API, then run the update with the `edit_sequence` from before | Answer the [conflict prompt](https://docs.qubesync.com/workflows.md#conflicts): `overwrite` or `keep` |
| Missing reference | `next_status: {code: 3140}` | Create the referenced record, or tell the user |
| QuickBooks closed | `connection_error: qb_closed` | Show `error.user_message`, not resend; the answer follows once you clear the fault |
| Company file moved or missing | `connection_error: company_file_missing` | Show `error.user_message`; your customer opens the right file or fixes the Web Connector's entry |
| Wrong company file | `connection_error: company_file_mismatch` | Show `error.user_message` |
| Computer off, or QuickBooks closed for long | `stalled: not_checking_in` or `quickbooks_unavailable`, then queue a request | Tell your customer by the `stall_reason` on `connection.stalled`, and clear the warning on `connection.resumed` |
| Slow QuickBooks | `latency_ms: 20000` | Keep your UI responsive while it waits |
| Large import | `iterator=true&max_returned=2` | Handle several pages, out of order |
| Your webhook endpoint down | stop your server, then queue a request | Recover from QuBe Sync's retries and not process a repeat twice |

## Then test against the real thing

The simulator models QuickBooks' behavior, not every corner of it. Before you launch, run your integration
against a real copy of QuickBooks Desktop: Intuit gives developers a free
[not-for-resale license](https://docs.qubesync.com/nfr-quickbooks.md).

> [!NOTE]
>
> In a sandbox app, an [iterated query](https://docs.qubesync.com/pagination.md) stops after 4 pages, with `iteration_state`
> `max_sandbox_iterations_reached`, on simulated and real connections alike.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.