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

# QuickBooks Online

> Client-mode REST API connector for the QuickBooks Online Accounting API: create invoices from production events, adjust inventory item quantities, query items and customers, pull change data capture and Profit and Loss, and refresh OAuth 2.0 tokens

Technical Preview

## Overview

A client-mode connector for the [QuickBooks Online Accounting API](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/account) (v3). Use it to close the loop between the shop floor and accounting: bill a customer when a batch ships, write scrap or consumption back as an inventory quantity adjustment, and read item stock levels or recent changes into MQTT.

The file holds two connectors. `QuickBooksOnlineRestApi` talks to the accounting API, and `QuickBooksOnlineOAuth` refreshes the OAuth access token on Intuit's token host.

| Event | Request | Notes |
| - | - | - |
| GetCompanyInfo | `GET /v3/company/{realm}/companyinfo/{realm}` | Smoke and realm check |
| Query | `GET /v3/company/{realm}/query?query=` | SQL-like query, URL-encoded (for example `select * from Item`) |
| ReadItem | `GET /v3/company/{realm}/item/{id}` | `QtyOnHand` for inventory items |
| CreateItem | `POST /v3/company/{realm}/item` | Create, or sparse update with `Id` and `SyncToken` |
| CreateInventoryAdjustment | `POST /v3/company/{realm}/inventoryadjustment` | Item quantity adjustment (`QtyDiff`) |
| CreateInvoice | `POST /v3/company/{realm}/invoice` | `CustomerRef` and `Line[]` |
| ReadInvoice | `GET /v3/company/{realm}/invoice/{id}` | |
| SendInvoice | `POST /v3/company/{realm}/invoice/{id}/send?sendTo=` | Emails the invoice; empty body |
| ReadCustomer | `GET /v3/company/{realm}/customer/{id}` | |
| CreateCustomer | `POST /v3/company/{realm}/customer` | `DisplayName` must be unique |
| ChangeDataCapture | `GET /v3/company/{realm}/cdc?entities=&changedSince=` | Changes in the last 30 days |
| Batch | `POST /v3/company/{realm}/batch` | Up to 30 operations per call |
| ProfitAndLossReport | `GET /v3/company/{realm}/reports/ProfitAndLoss?start_date=&end_date=` | |
| RefreshToken | `POST /oauth2/v1/tokens/bearer` (on `oauth.platform.intuit.com`) | New access token and refresh token |

Every accounting call adds `minorversion=75`. Since August 2025 QuickBooks ignores minor versions 1 to 74 and answers as 75, so the template pins the current base.

Set `BASE_ADDRESS` to `https://quickbooks.api.intuit.com` for real companies or `https://sandbox-quickbooks.api.intuit.com` for sandbox companies. Topics use `finance/quickbooks/<object>/<verb>`; replies land on `<topic>/response`.

See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes).

> [!NOTE]
>
> **Auth:** QuickBooks Online uses OAuth 2.0 authorization code with the `com.intuit.quickbooks.accounting` scope. Do the one-time consent outside the broker (the Intuit [OAuth 2.0 Playground](https://developer.intuit.com/app/developer/playground) or your own app) to get a refresh token and the company's `realmId`. After that the broker can stay connected on its own: publish to the RefreshToken event to get a fresh access token (valid 60 minutes) and send it as `access_token` with `realm_id` on every accounting call. Keep the client id, client secret and refresh token in Hub secrets (suggested names `QBO_CLIENT_ID`, `QBO_CLIENT_SECRET`, `QBO_REFRESH_TOKEN`, `QBO_REALM_ID`). Never commit them into the `.lot`.

> [!WARNING]
>
> The refresh token **rotates**. The token response can return a new `refresh_token`, and the old one stops working soon after. Always store the latest value. A refresh token that is not used for about 100 days expires, and the user has to reconnect.

## Basic Syntax

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE QuickBooksOnlineRestApi WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://quickbooks.api.intuit.com"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '60'
        WITH DEFAULT_CONTENT_TYPE "application/json"
        # Sandbox: https://sandbox-quickbooks.api.intuit.com. Auth: Bearer {value.json.access_token}; realm {value.json.realm_id}. Never commit tokens.
    ADD EVENT GetCompanyInfo
        WITH SOURCE_TOPIC "finance/quickbooks/company/info/get"
        WITH DESTINATION_TOPIC "finance/quickbooks/company/info/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/companyinfo/{value.json.realm_id}?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT Query
        WITH SOURCE_TOPIC "finance/quickbooks/query"
        WITH DESTINATION_TOPIC "finance/quickbooks/query/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/query?query={value.json.query}&minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT ReadItem
        WITH SOURCE_TOPIC "finance/quickbooks/item/get"
        WITH DESTINATION_TOPIC "finance/quickbooks/item/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/item/{value.json.item_id}?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT CreateItem
        WITH SOURCE_TOPIC "finance/quickbooks/item/create"
        WITH DESTINATION_TOPIC "finance/quickbooks/item/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/item?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT CreateInventoryAdjustment
        WITH SOURCE_TOPIC "finance/quickbooks/inventoryadjustment/create"
        WITH DESTINATION_TOPIC "finance/quickbooks/inventoryadjustment/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/inventoryadjustment?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT CreateInvoice
        WITH SOURCE_TOPIC "finance/quickbooks/invoice/create"
        WITH DESTINATION_TOPIC "finance/quickbooks/invoice/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/invoice?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT ReadInvoice
        WITH SOURCE_TOPIC "finance/quickbooks/invoice/get"
        WITH DESTINATION_TOPIC "finance/quickbooks/invoice/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/invoice/{value.json.invoice_id}?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT SendInvoice
        WITH SOURCE_TOPIC "finance/quickbooks/invoice/send"
        WITH DESTINATION_TOPIC "finance/quickbooks/invoice/send/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/invoice/{value.json.invoice_id}/send?sendTo={value.json.send_to}&minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/octet-stream"
            ADD "Authorization" "Bearer {value.json.access_token}"
        WITH BODY ""
    ADD EVENT ReadCustomer
        WITH SOURCE_TOPIC "finance/quickbooks/customer/get"
        WITH DESTINATION_TOPIC "finance/quickbooks/customer/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/customer/{value.json.customer_id}?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT CreateCustomer
        WITH SOURCE_TOPIC "finance/quickbooks/customer/create"
        WITH DESTINATION_TOPIC "finance/quickbooks/customer/create/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/customer?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT ChangeDataCapture
        WITH SOURCE_TOPIC "finance/quickbooks/cdc/get"
        WITH DESTINATION_TOPIC "finance/quickbooks/cdc/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/cdc?entities={value.json.entities}&changedSince={value.json.changed_since}&minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
    ADD EVENT Batch
        WITH SOURCE_TOPIC "finance/quickbooks/batch"
        WITH DESTINATION_TOPIC "finance/quickbooks/batch/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/batch?minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"
        WITH BODY "{value.json.body}"
    ADD EVENT ProfitAndLossReport
        WITH SOURCE_TOPIC "finance/quickbooks/reports/profitandloss/get"
        WITH DESTINATION_TOPIC "finance/quickbooks/reports/profitandloss/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/v3/company/{value.json.realm_id}/reports/ProfitAndLoss?start_date={value.json.start_date}&end_date={value.json.end_date}&minorversion=75"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Authorization" "Bearer {value.json.access_token}"

# OAuth 2.0 token refresh (different host). basic_auth = base64("<client_id>:<client_secret>") from Hub secrets.
# Reply: {"access_token":"...","expires_in":3600,"refresh_token":"...","x_refresh_token_expires_in":...,"token_type":"bearer"}.
# Store the returned refresh_token every time (it rotates). Errors: 400 invalid_grant (refresh token expired/revoked), 401 invalid_client.
DEFINE ROUTE QuickBooksOnlineOAuth WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://oauth.platform.intuit.com"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '30'
        WITH DEFAULT_CONTENT_TYPE "application/x-www-form-urlencoded"
    ADD EVENT RefreshToken
        WITH SOURCE_TOPIC "finance/quickbooks/oauth/refresh"
        WITH DESTINATION_TOPIC "finance/quickbooks/oauth/refresh/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/oauth2/v1/tokens/bearer"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "Content-Type" "application/x-www-form-urlencoded"
            ADD "Authorization" "Basic {value.json.basic_auth}"
        WITH BODY "grant_type=refresh_token&refresh_token={value.json.refresh_token}"
```

## MQTT examples

Control fields (`access_token`, `realm_id`, ids, `query`, dates) only fill the URL and headers. Only `body` is sent as the request body.

**RefreshToken** (`finance/quickbooks/oauth/refresh`). `basic_auth` is the Base64 of `client_id:client_secret`. Expect **200**:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{ "basic_auth": "<base64 client_id:client_secret>", "refresh_token": "<latest refresh token>" }
```

Response:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{ "token_type": "bearer", "access_token": "eyJlbmMiOi...", "expires_in": 3600, "refresh_token": "AB11...", "x_refresh_token_expires_in": 8726400 }
```

**Query** (`finance/quickbooks/query`). The query is placed in the URL, so URL-encode it. This asks for inventory items:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access token>",
  "realm_id": "9341455000000001",
  "query": "select%20*%20from%20Item%20where%20Type%20%3D%20%27Inventory%27%20MAXRESULTS%20100"
}
```

Response (trimmed):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{ "QueryResponse": { "Item": [ { "Id": "11", "Name": "Pump Seal Kit", "Type": "Inventory", "TrackQtyOnHand": true, "QtyOnHand": 10, "SyncToken": "0" } ], "startPosition": 1, "maxResults": 1 } }
```

**CreateInventoryAdjustment** (`finance/quickbooks/inventoryadjustment/create`). Writes scrap back to stock: two units off item 11, posted to the adjustment account 91. Expect **200** with the new `InventoryAdjustment`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access token>",
  "realm_id": "9341455000000001",
  "body": {
    "AdjustAccountRef": { "value": "91" },
    "TxnDate": "2026-10-05",
    "Line": [
      { "DetailType": "ItemAdjustmentLineDetail", "ItemAdjustmentLineDetail": { "ItemRef": { "value": "11" }, "QtyDiff": -2 } }
    ]
  }
}
```

Use a positive `QtyDiff` to add stock. Inventory adjustments need QuickBooks Online Plus or Advanced with quantity tracking turned on, and the item must be an `Inventory` item.

**CreateInvoice** (`finance/quickbooks/invoice/create`), expect **200** with the new `Invoice` (`Id`, `DocNumber`, `TotalAmt`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "access_token": "<access token>",
  "realm_id": "9341455000000001",
  "body": {
    "CustomerRef": { "value": "58" },
    "Line": [
      { "DetailType": "SalesItemLineDetail", "Amount": 150.0, "SalesItemLineDetail": { "ItemRef": { "value": "11" }, "Qty": 2, "UnitPrice": 75.0 } }
    ]
  }
}
```

**SendInvoice** (`finance/quickbooks/invoice/send`) takes `invoice_id` and a URL-encoded `send_to` address, for example `"send_to": "ap%40customer.example"`. The body is empty.

**Updates.** QuickBooks updates are sparse `POST`s to the create endpoint. Send the entity's `Id`, its current `SyncToken` and `"sparse": true` together with the fields to change. An old `SyncToken` returns **400** with code `5010`; read the entity again and retry.

## Keeping the token

A small action can keep the latest tokens when the refresh reply arrives:

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION QuickBooksKeepToken
ON TOPIC "finance/quickbooks/oauth/refresh/response" DO
    SET "access" WITH GET JSON "access_token" IN PAYLOAD AS STRING
    SET "refresh" WITH GET JSON "refresh_token" IN PAYLOAD AS STRING
    KEEP TOPIC "finance/quickbooks/session/access_token" WITH {access}
    KEEP TOPIC "finance/quickbooks/session/refresh_token" WITH {refresh}
```

Refresh before the access token expires (for example every 50 minutes), or when a call returns **401** with code `3200`. Restrict who can read the `finance/quickbooks/#` topics with a rule, because these payloads carry tokens and client credentials.

## Webhooks (companion, not packaged)

QuickBooks [webhooks](https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks) can `POST` entity-change notifications (for example Invoice, Item or Payment created or updated) to an HTTPS URL. Each request carries an `intuit-signature` header, an HMAC-SHA256 of the body with your app's verifier token, which the receiver should check. Receiving them needs a separate `ENABLE_SERVER` REST API connector on a publicly reachable HTTPS address. That companion connector is not part of this template. Change data capture (`ChangeDataCapture`) is the polling alternative.

## Notes

* Limits: 500 requests per minute and 10 concurrent requests per company and app; a batch holds at most 30 operations, with 40 batch calls per minute. Above that QuickBooks answers **429**.
* A query returns at most 1000 rows. Page with `STARTPOSITION` and `MAXRESULTS`, for example `select * from Item STARTPOSITION 1001 MAXRESULTS 1000`.
* Common errors: **401** code `3200` (expired or invalid token), **403** code `3100` (the app is not connected to that `realm_id`), **400** `ValidationFault` with codes such as `2500` (invalid reference), `6000` (business rule), `6240` (duplicate name), `5010` (stale `SyncToken`).
* Sandbox and production use different hosts and different companies. A sandbox token does not work on `quickbooks.api.intuit.com`.
* Not covered: invoice PDF download (binary), attachments (multipart upload), the Payments and Payroll APIs, and token revoke (`POST https://developer.api.intuit.com/v2/oauth2/tokens/revoke`).
* Suggested branches: `feat/rest-api-route-quickbooks-online`, `feat/docs-rest-api-quickbooks-online` (Hugo pushes).

## Related

* [REST API Connectors](/latest/lot-language/routes/rest-api-routes)
* [QuickBooks Online Accounting API reference](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice)
* [InventoryAdjustment entity](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/inventoryadjustment)
* [OAuth 2.0 for QuickBooks Online](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0)
* [Minor versions](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/minor-versions)
* [QuickBooks Online Accounting API Postman collection](https://www.postman.com/intuit-developer/intuit-developer-quickbooks-online-accounting-api/overview)


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