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

# QuickBooks

> Look up, create and update QuickBooks Online records, or create a bill from a verified purchase invoice match.

The QuickBooks node connects a workflow to QuickBooks Online. Pick an **Operation**: **Look up** reads a record in the middle of a run and hands it to the steps after it, **Insert** creates a record such as an invoice or a bill, **Update** writes approved values back to an existing record, and **Create Bill Against Purchase Order** posts a verified purchase invoice match as a bill. A following step is required after a lookup and optional after a write: a write can end a branch, or hand the created record to the steps after it.

## Configuration

| Field                 | Type             | Required                                 | Description                                                                                                                                                                                                                                                                                                          |
| --------------------- | ---------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Connector**         | select           | Yes                                      | A [connector](/admin/connectors) of type **QuickBooks Online**                                                                                                                                                                                                                                                       |
| **Operation**         | select           | Yes                                      | `Insert` (default) creates a new record, `Update` writes changes back to an existing record, `Look up` reads a record, `Create Bill Against Purchase Order` posts a matched [purchase invoice](/nodes/reconcile#purchase-invoice) (see [Operation](#operation))                                                      |
| **Entity**            | entity picker    | Yes (Insert, Update, Look up)            | The QuickBooks entity to read from or write to, for example `Invoice` (see [Entity selection](#entity-selection)). Bill creation inherits its purchase order from the matched result                                                                                                                                 |
| **Matched invoice**   | select           | Yes (Create Bill Against Purchase Order) | **Create Bill Against Purchase Order** only. The upstream Reconcile result (or the Review that continues it) on the Purchase invoice profile. Connection, vendor, invoice number, quantities and review policy are inherited                                                                                         |
| **Posting date**      | template field   | No                                       | **Create Bill Against Purchase Order** only. Leave blank to let QuickBooks date the bill                                                                                                                                                                                                                             |
| **Result**            | select           | Yes (Look up)                            | **Look up** only. `First match` (default) or `All matches` (see [Result](#result))                                                                                                                                                                                                                                   |
| **Rules**             | rule editor      | Yes (Look up)                            | **Look up** only. At least one. Each rule compares a field on the QuickBooks record against a value (see [Rules and operators](#rules-and-operators))                                                                                                                                                                |
| **Select**            | field picker     | No                                       | **Look up** only. Field names to return, picked from the connector's catalog or typed. Leave empty to return every field the connector sends                                                                                                                                                                         |
| **Body format**       | select           | No                                       | **Insert** / **Update** only. In **Insert**: `JSON` (default) posts the entity's fields, `Binary (base64)` sends raw bytes. QuickBooks does not support OData Batch. **Update** always uses the `Operations` body format automatically: the body is the operations document described below, not a plain JSON entity |
| **Body**              | template editor  | No                                       | **Insert** / **Update** only. Auto-generated from the entity and operation; hand edits are kept (see [Body template](#body-template))                                                                                                                                                                                |
| **Duplicate check**   | toggle           | No                                       | **Insert** only; not available in Update, and a lookup has nothing to guard. Off by default (see [Duplicate check](#duplicate-check))                                                                                                                                                                                |
| **Headers**           | key-value editor | No                                       | Custom headers, under **Advanced**                                                                                                                                                                                                                                                                                   |
| **Timeout (seconds)** | number           | No                                       | Request timeout in seconds. A whole number from 1 to 300 for a lookup; leave blank to use the system default (45s)                                                                                                                                                                                                   |

<Note>Transient delivery failures are retried automatically by the platform. Use the node's **retry** field to set the maximum number of attempts, between 1 and 5 (default 3).</Note>

<Note>
  At least one rule is required in **Look up**. A lookup with no rules would read the whole table, so the node refuses to save until you add one.
</Note>

## Connector

Pick a [connector](/admin/connectors) of type **QuickBooks Online**. The connector supplies the OAuth token and the realm, so the node needs no URL or HTTP method: Ingestly builds the request from the connector and the entity you choose.

Pointing this node at a Business Central connector fails the step rather than quietly reading from Business Central. A connector with no base URL fails the step with a message saying so: reconnect it first.

## Entity selection

QuickBooks Online does not publish a fetchable schema document, so the **Entity** field is populated from a built-in catalog. The catalog covers two groups:

* **Master data**, the tables a workflow usually looks a record up in: `Customer`, `Vendor`, `Item` and `Account`.
* **Transactions**: `Invoice`, `Estimate`, `SalesReceipt`, `CreditMemo`, `Payment`, `RefundReceipt`, `Bill`, `PurchaseOrder`, `BillPayment` and `VendorCredit`.

How the picker behaves:

* Once the catalog loads, search the list to pick an entity, or type a name and choose **Use "\<name>"** to enter one that is not listed.
* With the catalog loaded, click the refresh action next to the field to re-request it. Since the catalog is built in rather than fetched from QuickBooks, its contents only change when Ingestly adds support for more entities.
* If the catalog fails to load (or none has loaded yet), the field falls back to a plain text input with no refresh action: type the entity name directly once the field is no longer disabled.

For a **Look up**, the picker keeps the full read catalogue; for a write it offers only the ten transaction entities.

<Note>
  In **Update** an operation naming an entity outside those ten fails the step, naming the operation, before anything is sent. **Insert** checks no such list: an entity you type in by hand is sent as typed, and QuickBooks decides whether it accepts the write.
</Note>

The entity has to be a plain identifier: letters, digits and underscores, starting with a letter or an underscore. Spaces and slashes around the name are trimmed off before that check, so `/Vendor/` is stored and read as `Vendor`. A dot, a space inside the name, a slash inside the name, a leading digit, a template expression, or anything else is refused when you save. A name that is nothing but slashes or spaces is refused as a missing entity. Casing is forgiving: `customer` resolves to the catalog's `Customer`.

That trimming applies to **Entity** only. An entry in **Select** has to be a plain identifier on its own, with no surrounding slashes to strip.

## Operation

### Look up

Reads a record out of QuickBooks Online in the middle of a run and hands it to the steps after it. Use it to resolve something the document only refers to: extract a bill, find the vendor or the purchase order it names, then [reconcile](/nodes/reconcile) the two.

#### Result

**First match** (default). Asks QuickBooks for a single record. The matched record *is* the payload, so a downstream reference reads a field straight off it: `{{FindVendor.payload.DisplayName}}`. When nothing matched, `payload` is `null`.

**All matches**. Asks for a page of records. The payload is the array itself, so `{{FindVendor.payload}}` is what a [Loop](/nodes/loop) or a `$each` iterates, and `{{FindVendor.payload[0].DisplayName}}` reads the first row. When nothing matched, the payload is an empty array rather than `null`, so a `$each` over it is a no-op instead of a resolution failure.

<Warning>
  **All matches** returns at most 100 records, and there is no paging. A query that matches more than 100 returns the first 100 and reports `matchCount` as 100, so `matchCount` alone cannot tell a query that matched exactly 100 from one that matched thousands. `metadata.quickBooks.truncated` is what tells them apart: it is `true` only when records were left behind. Narrow the rules, or branch on `truncated`, when the answer has to be complete.
</Warning>

#### Rules and operators

A rule has three parts: a **Field**, an **Operator** and a **Value**. Rules combine with **AND**: a record has to satisfy every rule to match.

* **Field** names a column on the QuickBooks record, not a path into upstream data. Pick it from the connector's catalog for the chosen entity, or type it. It has to be a plain identifier, or a dotted path of plain identifiers, because QuickBooks has real nested query fields such as `MetaData.LastUpdatedTime`. A template expression in this box is refused.
* **Value** is the template-bearing half, and that is the inverse of a [Validate](/nodes/validation) rule. `{{extract.payload.vendor_name}}` in the value box is the normal case.

QuickBooks publishes nine operators:

| Operator                  | Matches records whose field                  |
| ------------------------- | -------------------------------------------- |
| **Equals**                | equals the value                             |
| **Greater Than**          | is greater than the value                    |
| **Greater Than or Equal** | is greater than or equal to the value        |
| **Less Than**             | is less than the value                       |
| **Less Than or Equal**    | is less than or equal to the value           |
| **Contains**              | contains the value's text                    |
| **Starts With**           | starts with the value's text                 |
| **Ends With**             | ends with the value's text                   |
| **In List**               | is one of the listed values, comma separated |

Every one of them requires a value.

##### Three operators this operation does not have

The [Business Central node's Look up operation](/nodes/business-central#look-up) publishes twelve operators. This one publishes nine. **Not Equals**, **Is Empty** and **Is Not Empty** are missing, because Intuit's documented operator list has no equivalents for them. Ingestly refuses all three when you save and again when the query is compiled, so there is no way to reach them by hand-editing a workflow either.

<Warning>
  **A QuickBooks lookup has no way to express "this field has a value" or "this field is empty".** There is no operator for either test, and no combination of the nine that stands in for one. If a workflow has to branch on whether a QuickBooks field is populated, read the record with a rule you *can* express, then test the field downstream with an [If](/nodes/if) node, whose **is empty** and **is not empty** operators work on data already in the run.
</Warning>

##### A literal `%` is refused

`%` is the wildcard in QuickBooks' query language, and there is no confirmed way to search for a literal one. So **Contains**, **Starts With** and **Ends With** refuse a value containing `%`, in two places:

* **When you save.** A `%` typed anywhere into the value of one of those three rules is rejected with an error naming the field and the operator. You cannot save the workflow until you remove it.
* **When the step runs.** A template that *resolves* to a value containing `%` fails the run. `{{extract.payload.terms}}` is clean when you save it, and if the extracted text turns out to be `50% deposit`, the step fails with the same message.

<Warning>
  The second half is the one you actually hit. Nothing can see inside a template at authoring time, so a workflow bound to extracted text saves without a word and then fails on the first document whose text happens to contain a percent sign. If a value could carry a `%`, strip it in a [Transform](/nodes/transform) step before the rule reads it, or match on a field that cannot contain one. Stripping it inline does not work: the save-time check reads the value text as typed, so `{{extract.payload.terms | replace: '%', ''}}` is refused because the filter's own argument contains a `%`. Switching the rule to **Equals** or **In List** avoids the restriction entirely; only the three text-search operators refuse a `%`.
</Warning>

Business Central has no such restriction: its OData text functions have no wildcard semantics, so `%` there is an ordinary character.

<Note>
  Every rule value is written into the query as a quoted literal, which is what Intuit's own documented examples do for numbers and dates as well as text. There is no value type to choose here, and the node refuses one if an import or the assistant writes it in.

  This is the one place the two lookups differ on values. The [Business Central node's Look up operation](/nodes/business-central#value-types) has a **Value Type** control, because its OData filter rejects a quoted literal against a numeric, date or boolean column.
</Note>

##### Values that resolve to nothing

A value box is filled in at save time, but it is resolved at run time, and a template can resolve to nothing. For five of the nine operators, an empty resolved value would match every record in the table, so the step fails instead:

* **Contains**, **Starts With**, **Ends With**, **Greater Than** and **Greater Than or Equal** refuse an empty resolved value, naming the field and the operator.
* **Equals**, **Less Than** and **Less Than or Equal** are allowed through, because those stay narrow rather than widening.
* **In List** needs at least one non-empty entry after the commas are split.

This is a run-time failure, not a save-time one: a rule using one of those five operators, bound to a field the extract did not produce, saves cleanly and fails when it runs. The other three do not fail. An **Equals**, **Less Than** or **Less Than or Equal** rule whose value resolves to nothing compares the field against an empty string, so the lookup runs and, unless a record's field is itself blank, reports `found` as `false`: a missing extraction reads as "not found", and nothing tells you the question was never asked. Bind the value to a field you know the upstream step emits, and treat `found: false` after an Equals rule as "check the extraction" as well as "check the company".

##### Reserved field names

`null`, `true` and `false` are literals in QuickBooks query expressions rather than field names, so a rule whose field is exactly one of them is refused when you save, in any casing. QuickBooks' own grammar would read the word as a literal rather than as your column, so the rule would compare something other than the field you meant.

#### Select

**Select** limits which fields come back. Leave it empty to take everything the connector sends. Pick each name from the connector's catalog, or type it and choose **Add**. Each entry is a plain identifier, and unlike a rule's field it does not accept a dotted path.

QuickBooks returns exactly the fields you select. If the record feeds a QuickBooks node in **Update**, through a [Reconcile](/nodes/reconcile) or directly, keep `Id` and `SyncToken` in the list: the write-back reads the record id from `Id`, and every QuickBooks update operation has to carry the record's `SyncToken`.

<Note>
  There is no **Expand** field on this node. Expand is an OData concept and QuickBooks has no OData surface. The [Business Central node's Look up operation](/nodes/business-central#select-and-expand) has one.
</Note>

#### When nothing matches

**A lookup that matches nothing is a successful step, not an error.** The step completes, `payload` is empty (`null` in **First match**, `[]` in **All matches**), and `metadata.quickBooks.found` is `false`.

That is deliberate. "This vendor does not exist in QuickBooks" is a business outcome your workflow should route on, not an outage. Only a query that could not be *asked* fails the step: a bad connector, a rejected query, an HTTP error from QuickBooks, or a lookup that did not answer within its **Timeout** (45 seconds unless you set one).

Route on it with the node's own ports. In **Look up** the node has no plain output: it leaves through one of two ports instead.

| Port          | Fires when                                                           |
| ------------- | -------------------------------------------------------------------- |
| **Found**     | At least one record matched                                          |
| **Not found** | Nothing matched (in **All matches**, the payload is the empty array) |

With **Fail if not found** off, exactly one of the two fires. Connect the path that uses the record to **Found** and whatever "we could not find it" should do to **Not found**: create the record with a QuickBooks node in **Insert**, send it to a [Review](/nodes/review) for a human to resolve, or nothing at all, since a run that leaves through an unconnected port simply stops there and still completes. At least one of the two has to be connected. With **Fail if not found** on, connect **Found** to the next step; no matches fail the step instead of taking **Not found**.

#### Fail if not found

Enable **Fail if not found** when a matching record is required. With it on, no matches fail the step instead of taking the **Not found** branch; leave **Continue on fail** off and the error output unconnected to stop the run. It is off by default, keeping the **Found** / **Not found** branches, applies to both result modes, and leaves connection and API errors on their normal error handling.

<Warning>
  Do not test for a missing record with an [If](/nodes/if) node that reads `{{FindVendor.payload.DisplayName}}`. A no-match payload is `null`, so a reference through it resolves to nothing, which is the same thing a genuine record with a blank `DisplayName` produces. The **Not found** port is the routing answer; `metadata.quickBooks.found` is the field to read when a later step needs the fact inside a template.
</Warning>

### Insert

The default operation. Creates a new record from the current document. The body auto-scaffolds the entity's fields plus its `Line` array, built from the columns carried into this step.

### Update

Writes changes back to an existing QuickBooks Online record. The body is an **operations document**, the same idea as Business Central's, but a different shape: QuickBooks has no addressable line records, so every operation is a whole-entity `create`, `update`, or `delete`. There is no separate line operation.

#### Record source

Choose where the target record's id, and its line rows, come from.

**Reconcile** (default, recommended). Ingestly builds the write plan from an upstream [Reconcile](/nodes/reconcile) step's verified match, and keeps its safety gate.

| Field                                   | Type            | Required | Description                                                                                                                                                                            |
| --------------------------------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Reconcile node**                      | select          | Yes      | The upstream Reconcile step whose approved values are written back                                                                                                                     |
| **Record id path** (under **Advanced**) | template editor | Yes      | Path to the record's id inside the connector record Reconcile fetched. Defaults to `{{Id}}` for QuickBooks; override only if this entity's id field differs                            |
| **Line id path** (under **Advanced**)   | template editor | No       | Path to a matched connector line's id, scoped to that single line row, not the whole record. Defaults to `{{Id}}` for QuickBooks; override only if this entity's line id field differs |
| **Line id column** (under **Advanced**) | text            | No       | The output key the matched line's id is written under, so the body can read it back as `{{$item.<name>}}`. Defaults to `lineId`                                                        |

<Note>Line id path and Line id column work together: the server stamps a matched or removed line row's connector line id only when both are set, reading the id from Line id path and writing it under the Line id column key. If the two disagree (for example, the column renamed to `rowId` while the body still reads `{{$item.lineId}}`), the id lands where nothing looks; left blank, line operations resolve their id to nothing. QuickBooks defaults both fields once a connector and entity are chosen, so leaving them alone is normally correct: open **Advanced** only if this entity's line id field differs from the default.</Note>

<Note>Two safety behaviors apply: if the reconcile result has a blocking mismatch and no approved review, the step fails instead of writing; if nothing changed and there is nothing to send, the step succeeds without sending a request.</Note>

**Direct** (opt-in). No Reconcile node needed. You supply the record id, and optionally the line arrays, directly.

| Field             | Type            | Required                                 | Description                                                                                                       |
| ----------------- | --------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Record id**     | template editor | Yes                                      | Any template expression that resolves to the connector record's id, for example `{{extract.payload.orderNumber}}` |
| **Lines**         | template editor | No                                       | An upstream array whose rows already carry a connector line id. Each row becomes a line update                    |
| **Added lines**   | template editor | No                                       | An upstream array of new lines to create. These rows have no line id yet                                          |
| **Removed lines** | template editor | No                                       | An upstream array whose rows already carry a connector line id. Each row becomes a line delete                    |
| **Line id field** | text            | Yes when Lines or Removed lines is bound | The row field that holds the connector line id, for example `lineId`                                              |

<Warning>Direct skips the Reconcile safety gate: whatever record id and line references resolve below are written as-is, with nothing verifying them against the source document first. Switch to Reconcile when that verification matters.</Warning>

For line pairing too complex for a template to express (for example, matching document lines to fetched connector lines by more than one key), add a [Transform](/nodes/transform) step upstream that joins the two sides and emits rows carrying both, then point Direct's **Lines** field at that array.

<Note>
  Direct never fetches the connector record, so `{{$writeback.record.*}}` has nothing to read: a body that references it under Direct is a design-time error. This matters most for QuickBooks, whose update operations require `values.SyncToken` (its concurrency token) and whose auto-generated body binds it from `{{$writeback.record.SyncToken}}`.

  Under Direct, bind `SyncToken` from whichever upstream node already supplied the record instead, for example `{{lookup.payload.SyncToken}}`.
</Note>

#### The operations document

Because QuickBooks has no addressable line records, `Line` is an embedded array on the document itself. A QuickBooks update is **one** operation whose `values.Line` array is generated by a `$each` directive over the write plan:

```json theme={null}
{
  "operations": [
    {
      "op": "update",
      "entity": "Invoice",
      "id": "{{$writeback.recordId}}",
      "values": {
        "SyncToken": "{{$writeback.record.SyncToken}}",
        "sparse": true,
        "Line": [
          {
            "$each": "{{$writeback.updates}}",
            "Id": "{{$item.lineId}}",
            "Amount": "{{$item.amount}}"
          }
        ]
      }
    }
  ]
}
```

<Warning>`parentEntity` and `parentId` are Business Central concepts. Setting either on a QuickBooks operation is a design-time error: QuickBooks has no line sub-resource for them to target. Add line rows to `values.Line` instead.</Warning>

The `$each` directive only expands because it sits as the array's own element (`"Line": [ { "$each": ... } ]`); the same object in a value position (`"Line": { "$each": ... }`) is not a directive and is written through literally. See [row expansion](/guides/expressions#row-expansion-in-json-bodies-each-and-when).

QuickBooks resends the full `Line` array on every update, so a line left out of it is treated as removed. The generated body only wires in `{{$writeback.updates}}` (matched lines); to also create new lines in the same write, add a second row template over `{{$writeback.adds}}` inside the array:

```json theme={null}
"Line": [
  { "$each": "{{$writeback.updates}}", "Id": "{{$item.lineId}}", "Amount": "{{$item.amount}}" },
  { "$each": "{{$writeback.adds}}", "Amount": "{{$item.amount}}" }
]
```

<Note>Use `{{$writeback.*}}`, not `{{writeback.*}}`. There is no reserved bare `writeback` head: an unprefixed reference is an ordinary node reference and fails if no node named `writeback` exists upstream. The `$` prefix is what lets a workflow also have a node named `writeback` with no ambiguity.</Note>

#### Atomicity

Business Central's `$batch` is transactional: every operation succeeds, or none do. QuickBooks is not. QuickBooks applies every operation in an operations document independently through its own batch endpoint, even when the document has only one operation. If a step's operations document has more than one operation (for example, a step that writes several records in one run), some can succeed while others fail.

<Warning>
  A partial failure fails the step instead of reporting success. Ingestly's error names which operations applied and which did not, plus any operation QuickBooks reported no outcome for. This step never retries a partial failure automatically, and a retry is not selective: it resends the entire configured body again, exactly as written, including every operation that already applied. Before you retry, edit the body so it contains only the operations that still need to run, then retry and confirm the resend.
</Warning>

### Create bill against purchase order

Use this operation after a **Purchase invoice** [Reconcile](/nodes/reconcile#purchase-invoice) whose purchase order source is a QuickBooks **Look up**. Choose the **Matched invoice** result. The node inherits the purchase order, vendor, invoice number, proposed quantities, connection and review policy from that match. You do not configure an entity, a body or individual line updates.

Leave **Posting date** blank to let QuickBooks date the bill, or point it at a date field. When price checks are off in Reconcile, the purchase order's own unit prices apply.

QuickBooks has no separate receiving step, so the bill is the whole posting. The operation creates one vendor **Bill** linked to the purchase order and to each billed purchase order line, using the **Create bill against purchase order** receiving mode in Reconcile. Receipt quantities and units are not part of this flow and are hidden in configuration and review.

<Warning>
  QuickBooks cannot validate purchase order balances and create the bill in one transaction. Ingestly rereads the purchase order and its linked transactions immediately before creating the bill and stops if anything changed, but a change made in QuickBooks inside that window is not locked out. Business Central posts atomically; QuickBooks does not. The capability status beside the matched invoice says so.
</Warning>

#### Configurations the operation refuses

Supported today: item-based purchase order lines, partial invoices, and prices, tax codes, customer, class and billable status carried from the purchase order line. Everything below is checked before any write and refuses the whole post rather than billing part of the order. Handle those orders in QuickBooks.

| Refused             | What triggers it                                                                                    |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| Account-based lines | A purchase order line that is not item based                                                        |
| Closed orders       | A purchase order whose status is not **Open**                                                       |
| Unverifiable links  | A linked bill, expense, check or purchase whose lines are not linked to single purchase order lines |
| Other linked types  | A linked transaction that is not a `Bill`, `Purchase`, `Expense` or `Check`                         |
| Mixed vendors       | A linked transaction belonging to a different vendor                                                |
| Duplicate invoices  | A bill already exists for this vendor with the same invoice number                                  |
| Oversized orders    | More than 2,000 lines, or more than 200 linked transactions                                         |

Quantities already billed come from the purchase order's linked transactions. If those links cannot be read line by line, the balance stays unknown and blocks the match rather than being treated as zero. Billing more than was ordered needs an [over-receipt allowance](/nodes/reconcile#over-receipt-allowance) in Reconcile, which acts as an over-billing allowance here; QuickBooks has no code of its own to permit it.

**Preview** and **Test Step** do not post. A successful run returns the confirmed bill, the operation ID and the posting time. The receipts list is always empty for QuickBooks. The output also carries the purchase order after posting (`order`) with its number and status (`open`, `closed`, or `unknown` when QuickBooks could not be read again after the bill was confirmed); QuickBooks keeps no archived order versions, so `order.archive` is always null.

Every attempt is sent with a stable request ID and carries a marker identifying the saved operation. Before creating anything, a retry looks for a bill carrying that marker, so an interrupted attempt is recovered rather than repeated. If QuickBooks refuses the bill, or a duplicate already exists, the step reports why and stays retryable once you fix it in QuickBooks. If QuickBooks leaves the outcome unconfirmed, the step reports **Recovery required** and sends nothing further. Do not create a second bill to work around an unresolved posting result.

To build the complete flow, pick **Post a purchase invoice against a purchase order** in the workflow templates with a QuickBooks connection. Applying the template replaces the canvas in one undoable edit.

## Duplicate check

Applies when **Operation** is `Insert`; the panel is not available in Update, and a lookup has nothing to guard. An Update writes to a record that already exists, so a lost response can't be resolved by looking the record up the same way a lost create can; it fails the step rather than risk a wrong resolution.

<Note>
  Turn on **Duplicate check** to identify the record this step creates, so that if the connection drops after QuickBooks accepts the write but before Ingestly sees the response, a repeat of that attempt can be resolved instead of guessed at. Leave it off and an unresolved attempt like that fails instead.

  * **Entity:** defaults to the node's configured entity.
  * **Keys:** one or more field/value pairs that identify the record by its natural key, for example `Field: DocNumber`, `Value: {{extract.payload.invoiceNumber}}`.
</Note>

<Warning>QuickBooks does not yet support automatic recovery. When a delivery outcome is unknown, Ingestly cannot look the record up, so the attempt fails rather than risk creating a duplicate. Configuring a duplicate check on this node protects against a false duplicate; it does not yet resolve the unknown attempt for you.</Warning>

## Body template

The body auto-generates from the entity, operation, and the columns carried into this step. Edit the body directly and your changes are preserved: the scaffold only regenerates while the body is still exactly what it last generated, so a hand-edited body is never overwritten. Use the **Generate body** action to reset a body back to a fresh scaffold.

### Advanced

The **Advanced** section exposes the raw body template editor (with autocomplete and validation) and the header editor. JSON bodies support [row expansion with `$each`, `$when`, and `$case`](/guides/expressions#row-expansion-in-json-bodies-each-and-when).

## Inputs and outputs

**Allowed inputs:** any trigger, and any action node that produces a result payload (extract, validation, transform, filter, split, classify, merge, parse, review, wait, if, switch, loop, variable, store, prompt, reconcile, HTTP, call workflow, Business Central, QuickBooks, callback). A QuickBooks node can feed a QuickBooks node: look the vendor up, then write the bill.

**Output:** An **Insert** or **Update** sends the request to QuickBooks Online and makes the response available to later steps. Request-level fields land under `metadata.quickBooks` (`statusCode`, `method`, `url`, `attempts`, `requestSucceeded`). A following step is optional: the node can end a branch, or feed the steps after it.

A **Look up** hands the matched record (or the array of them) down as `payload`, with the outcome under `metadata.quickBooks` (`found`, `matchCount`, `truncated`, `statusCode`). It leaves through its **Found** or **Not found** port rather than a plain output (see [When nothing matches](#when-nothing-matches)), so every following step hangs off one of the two. A lookup costs 0 credits, and a [dry run](/guides/test-mode) does not skip it: the query reaches your live company. It reads a record; it does not read the document.

## Using the result downstream

**After an Insert or Update**, the node's response is available to later steps as `payload`, exactly as QuickBooks returned it: Ingestly does not unwrap or rename anything. The shape depends on the operation:

* **Insert** posts the record and QuickBooks answers with the created record under a key named after the entity, with its id at `Id`. For an `Invoice` the new id is `{{NodeName.payload.Invoice.Id}}`, and its concurrency token is `{{NodeName.payload.Invoice.SyncToken}}`. There is no `{{NodeName.payload.id}}` on a QuickBooks response, and autocomplete does not offer one.
* **Update** always goes through QuickBooks' batch endpoint, so the payload is a `BatchItemResponse` array with one entry per operation. Each entry carries its `bId` and the updated record under the entity key: `{{NodeName.payload.BatchItemResponse[0].Invoice.Id}}`.
* An **Update** with nothing to send (the reconcile found no changes) makes no request at all. The step succeeds with a `null` payload, `statusCode` 204 and `method` `NONE`.

**After a Look up**, `payload` is the connector record itself, with no wrapper around it, so a downstream reference names the QuickBooks field directly:

| Reference                               | Result mode | Value                                          |
| --------------------------------------- | ----------- | ---------------------------------------------- |
| `{{FindVendor.payload.DisplayName}}`    | First match | The matched record's `DisplayName` field       |
| `{{FindVendor.payload}}`                | All matches | The whole array of matched records             |
| `{{FindVendor.payload[0].DisplayName}}` | All matches | The first matched record's `DisplayName` field |

<Note>
  Ingestly does not know the shape of a QuickBooks record, so autocomplete stops at `payload`. The fields under it still resolve at run time; they are simply not offered as suggestions. Use **Select** to make it obvious which fields a step is meant to be reading.
</Note>

**`metadata.quickBooks`** carries the request and the outcome:

| Field                | Description                                                                                                                                              |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **statusCode**       | The HTTP response status code                                                                                                                            |
| **requestSucceeded** | **Insert** and **Update** only. Whether the request returned a 2xx status                                                                                |
| **url**              | **Insert** and **Update** only. The URL that was actually called. Emitted on every run but not offered by autocomplete; type it by hand                  |
| **method**           | **Insert** and **Update** only. The HTTP method that was used, in upper case: always `POST` for QuickBooks, or `NONE` when an Update had nothing to send |
| **attempts**         | **Insert** and **Update** only. The retry attempt number for this request. Emitted but not offered by autocomplete                                       |
| **found**            | **Look up** only. Whether anything matched                                                                                                               |
| **matchCount**       | **Look up** only. How many records came back (at most 100 in **All matches**)                                                                            |
| **truncated**        | **Look up** only. `true` when more records matched than came back. Always `false` in **First match**, which asks for one record by design                |

<Tip>
  A QuickBooks update needs the record's `SyncToken`. A **Look up** in **First match** mode is a clean way to fetch it: read it downstream as `{{FindInvoice.payload.SyncToken}}` and bind it into the [operations document](#the-operations-document) of a QuickBooks node in **Update**.
</Tip>

<Note>
  The node sits in the **Actions** group of the node picker. In **Insert** or **Update** it behaves as an output: a following step is optional, it cannot be disabled, and **Test Step** is not offered because a test would write to your live company. In **Look up** a following step is required, the node may be disabled, and Test Step runs the real query. Test Step is also refused on any node below an **Insert** or **Update**, since a test run executes every step above the one being tested.
</Note>

## Error output

Turn on **Error output** in the node's settings to add an **Error** port beside the node's other ports. When the write fails after its retries, or a lookup fails, the run follows that port instead of failing, so you can route the failure to a [Review](/nodes/review), a notification, or a compensating step. With no error edge drawn, a failed step fails the run exactly as before.

A no-match takes this port only when **Fail if not found** is on. With the option off, no matches are a successful lookup and take **Not found**.

<Note>
  Inside a [loop](/nodes/loop) body the **Error output** toggle is not offered and the error port never renders. Whether a failing item stops the loop is the loop's own **If an item fails** setting instead. See [Routing failures](/guides/conditional-routing#routing-failures).
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Reconcile" icon="scale-balanced" href="/nodes/reconcile">
    Compare the looked-up record against the document
  </Card>

  <Card title="Business Central" icon="building" href="/nodes/business-central">
    The same three operations against Business Central
  </Card>

  <Card title="If" icon="code-branch" href="/nodes/if">
    Branch on whether the lookup found anything
  </Card>

  <Card title="Connectors" icon="plug" href="/admin/connectors">
    Set up a QuickBooks Online connector
  </Card>
</CardGroup>
