QuickBooks Online
Accounting integration for invoices, payments, and financial data
QuickBooks Online
Connect to QuickBooks Online to automate invoice creation, payment processing, customer management, and financial reporting.
Overview
The existing Accounting connection exposes 198 searchable actions in the canvas and workflow builder. Chat receives the same action names, parameters, and outputs. The catalog covers the Intuit Accounting API reference, including 30 report endpoints. Availability depends on the connected company's region, subscription, and enabled features. QuickBooks Payments uses a separate API and authorization scope.
| Area | Available operations |
|---|---|
| Accounting records | Customers, vendors, accounts, items, invoices, payments, bills, bill payments, journal entries, purchases, purchase orders, credits, deposits, transfers, estimates and sales/refund receipts |
| Company and setup | Company info, preferences, currencies, exchange rates, classes, departments, employees, terms, payment methods, taxes and entitlements |
| Advanced records | Budgets, change orders, inventory adjustments, recurring transactions, time activities and reimbursement charges where Intuit supports them |
| Documents and bulk work | PDF export, attachment upload/download, note attachments, batch requests, change data capture and queries |
| Reporting | All 30 documented report endpoint names, with complete headers, columns and nested rows available to later workflow steps |
Configure an action
Choose QuickBooks, select the connected account, search Action, and set Company (Realm) ID. The form shows fields for that operation. Existing action IDs and input fields remain compatible with saved workflows.
New read actions use recordId. New create, update and delete actions use a Data JSON field with Intuit's field names. For example, Create Estimate accepts:
{
"CustomerRef": { "value": "123" },
"Line": [{
"Amount": 100,
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": { "ItemRef": { "value": "1" }, "Qty": 1, "UnitPrice": 100 }
}]
}Use IDs from the connected company. Updates and deletes require the record's current Id and SyncToken. Full updates replace omitted fields; use sparse: true only for actions whose Intuit reference supports sparse updates. The existing customer, invoice, bill, payment and journal-entry actions retain their individual fields and automatic token reads where already supported.
Create Item takes the item in Data. Name is required. A NonInventory or Service item also needs IncomeAccountRef; an Inventory item needs Intuit's inventory fields as well (TrackQtyOnHand, QtyOnHand, InvStartDate, AssetAccountRef, ExpenseAccountRef). QuickBooks requires the name to be unique across items, so query by name before creating one. For example:
{
"Name": "Gateway 2.0",
"Type": "NonInventory",
"IncomeAccountRef": { "value": "358" },
"Sku": "GW-2",
"Description": "Gateway 2.0",
"UnitPrice": 399.99,
"Taxable": true
}Batch accepts 1–30 batchItems with unique bId values. Inspect every BatchItemResponse for Fault, even when the HTTP request succeeds. Change Data Capture accepts entities and a changedSince ISO timestamp within the previous 30 days.
PDF actions return contentBase64, contentType, and size. Upload Attachment accepts a filename, MIME type, base64 content (up to 20 MB), and optional QBO attachment metadata. Download Attachment returns a temporary downloadUrl. Get Entitlements preserves the XML response as entitlementsXml.
Prerequisites
- QuickBooks Online account (Plus, Essentials, or Simple Start)
- Admin access to configure OAuth connection
- Company file (Realm ID) for API access
Authentication
QuickBooks uses OAuth 2.0 for authentication via Nango.
Connect QuickBooks
# Get OAuth URL
curl "https://workflow.loopfour.ai/api/v1/connections/quickbooks/auth-url" \
-H "x-api-key: YOUR_API_KEY"Required Scopes
| Scope | Description |
|---|---|
com.intuit.quickbooks.accounting | Full accounting access |
Available Actions
Customer Actions
createCustomer
Create a new customer in QuickBooks.
{
"id": "create-customer",
"type": "action",
"action": "quickbooks.createCustomer",
"config": {
"displayName": "{{input.companyName}}",
"firstName": "{{input.firstName}}",
"lastName": "{{input.lastName}}",
"companyName": "{{input.companyName}}",
"email": "{{input.email}}",
"phone": "{{input.phone}}",
"billingAddress": {
"line1": "{{input.address.street}}",
"city": "{{input.address.city}}",
"state": "{{input.address.state}}",
"postalCode": "{{input.address.zip}}",
"country": "{{input.address.country}}"
}
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
displayName | string | Yes | Display name (unique identifier) |
firstName | string | No | First name |
lastName | string | No | Last name |
companyName | string | No | Company name |
email | string | No | Primary email address |
phone | string | No | Phone number |
mobile | string | No | Mobile number |
billingAddress | object | No | Billing address |
notes | string | No | Internal notes |
active | boolean | No | Active status (default: true) |
getCustomer
Get a customer by ID.
{
"action": "quickbooks.getCustomer",
"config": {
"customerId": "{{input.customerId}}"
}
}updateCustomer
Update an existing customer.
{
"action": "quickbooks.updateCustomer",
"config": {
"customerId": "{{steps.lookup.output.id}}",
"email": "{{input.newEmail}}"
}
}findCustomerByEmail
Find a customer by email address.
{
"action": "quickbooks.findCustomerByEmail",
"config": {
"email": "{{input.email}}"
}
}listCustomers
List customers with optional filters.
{
"action": "quickbooks.listCustomers",
"config": {
"active": true,
"companyName": "Acme",
"limit": 50,
"offset": 0
}
}Invoice Actions
createInvoice
Create a new invoice.
{
"id": "create-invoice",
"type": "action",
"action": "quickbooks.createInvoice",
"config": {
"customerId": "{{steps.customer.output.Customer.Id}}",
"lines": [
{
"description": "Professional Services",
"amount": 1500.00,
"quantity": 10,
"unitPrice": 150.00
},
{
"description": "Software License",
"amount": 500.00,
"quantity": 1,
"unitPrice": 500.00,
"itemId": "123"
}
],
"dueDate": "2024-02-15",
"invoiceDate": "2024-01-15",
"invoiceNumber": "INV-001",
"customerMemo": "Thank you for your business!",
"email": "{{input.customerEmail}}"
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
lines | array | Yes | Line items (at least one) |
lines[].description | string | No | Line item description |
lines[].amount | number | Yes | Line amount |
lines[].quantity | number | No | Quantity (default: 1) |
lines[].unitPrice | number | No | Unit price |
lines[].itemId | string | No | Product/Service ID |
lines[].taxCodeId | string | No | Tax code (TAX, NON, or a tax-code id) → SalesItemLineDetail.TaxCodeRef; requires txnTaxDetail |
dueDate | string | No | Due date (YYYY-MM-DD) |
invoiceDate | string | No | Invoice date (default: today) |
invoiceNumber | string | No | Custom invoice number |
customerMemo | string | No | Memo for customer |
privateNote | string | No | Internal note |
email | string | No | Email to send invoice to |
txnTaxDetail | object | No | { totalTax, txnTaxCodeId } — both required → TxnTaxDetail (see below) |
shipAddr | object | No | { line1, line2?, city?, countrySubDivisionCode?, postalCode?, country? } → ShipAddr |
billAddr | object | No | Same shape → BillAddr |
Externally computed sales tax. A company on US Automated Sales Tax lets QuickBooks compute tax from the ship-to address. To carry a tax computed elsewhere (for example by Avalara) instead, send the override as one unit: txnTaxDetail.totalTax, txnTaxDetail.txnTaxCodeId, and a taxCodeId on every line. Intuit only honours the override when TxnTaxDetail.TotalTax and TxnTaxDetail.TxnTaxCodeRef are both present — a totalTax-only body returns 200 and QuickBooks silently recomputes the tax — so each of these is rejected before the request is sent:
txnTaxDetailwithouttxnTaxCodeId;txnTaxDetailwith any line missingtaxCodeId(a sparse update replaces the wholeLinearray, which would strand a staleTxnTaxDetail);- a line
taxCodeIdwithouttxnTaxDetail; - a
totalTaxlarger than the sum of the line amounts.
totalTax must be a finite number ≥ 0, each address field must be a string or number, and each address needs at least line1. All three fields are optional and leave the request unchanged when absent. The Invoice.TxnTaxDetail.TotalTax returned by the write is then compared against what was submitted — both rounded to cents, so they must agree to within one cent — and the step fails on a mismatch rather than reporting a success QuickBooks did not honour. QuickBooks returns the persisted transaction in its create/update response, so a recompute shows up there and no second fetch is needed.
{
"action": "quickbooks.createInvoice",
"config": {
"customerId": "{{steps.customer.Customer.Id}}",
"lines": [{ "amount": 100, "itemId": "12", "taxCodeId": "TAX" }],
"shipAddr": { "line1": "1 Main St", "city": "Austin", "countrySubDivisionCode": "TX", "postalCode": "78701", "country": "US" },
"txnTaxDetail": { "totalTax": "{{steps.avalara.totalTax}}", "txnTaxCodeId": "5" }
}
}Intuit documents this override at create time. The update path of upsertInvoiceByDocNumber sends the same fields but has not been verified against a live company; a taxed invoice also cannot later be un-taxed through the upsert, because both fields are now required together.
getInvoice
Get an invoice by ID.
{
"action": "quickbooks.getInvoice",
"config": {
"invoiceId": "{{input.invoiceId}}"
}
}sendInvoice
Send an invoice to the customer via email.
{
"action": "quickbooks.sendInvoice",
"config": {
"invoiceId": "{{steps.create-invoice.output.Invoice.Id}}",
"email": "{{input.alternateEmail}}"
}
}voidInvoice
Void an invoice.
{
"action": "quickbooks.voidInvoice",
"config": {
"invoiceId": "{{input.invoiceId}}"
}
}updateInvoice
Update an existing invoice by its QuickBooks id. The request is sparse, so only
the supplied fields change — except lines, which replaces the whole Line
array (QuickBooks has no per-line patch).
{
"action": "quickbooks.updateInvoice",
"config": {
"invoiceId": "{{input.invoiceId}}",
"dueDate": "2024-03-01",
"customerMemo": "Updated payment terms"
}
}| Field | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | QuickBooks invoice Id |
lines | array | No | Replaces the entire Line array when supplied |
lines[].taxCodeId | string | No | Tax code (TAX, NON, or a tax-code id) → SalesItemLineDetail.TaxCodeRef; requires txnTaxDetail |
dueDate | string | No | Due date (YYYY-MM-DD) |
customerMemo | string | No | Memo for customer |
privateNote | string | No | Internal note |
email | string | No | Billing email |
txnTaxDetail | object | No | { totalTax, txnTaxCodeId } — both required → TxnTaxDetail; requires lines |
shipAddr | object | No | { line1, line2?, city?, countrySubDivisionCode?, postalCode?, country? } → ShipAddr |
billAddr | object | No | Same shape → BillAddr |
Because the sparse update replaces the whole Line array, an update that
re-sends lines without taxCodeId and without txnTaxDetail would drop tax a
previous create attached and let Automated Sales Tax recompute it.
Four of the rules below are createInvoice's coupling rules, and they run
before the invoice is read, so bad input never costs a request:
txnTaxDetailwith nolinesis rejected — there would be no lines to code;txnTaxDetailwith any line missingtaxCodeIdis rejected;- a line
taxCodeIdwith notxnTaxDetailis rejected; - a
totalTaxlarger than the line total (compared in cents) is rejected.
Each of those four needs a tax field to be present, so on their own they do
nothing about the case that actually loses the tax: an update carrying lines
and no tax input at all. The fifth rule is the one that covers it, and it
reads the stored invoice rather than the config:
- supplying
lineswith notxnTaxDetailagainst an invoice that shows any sign of stored tax is rejected after the read and before the write. The error names the field it read and the remedy: resendtxnTaxDetailtogether with a per-linetaxCodeId, both recomputed for the new lines. This costs no extra request — the read that fetches the SyncToken returns the whole invoice.
Any one of these signals is enough, because QuickBooks does not populate them together:
| Signal | Why it alone is not enough |
|---|---|
TxnTaxDetail.TotalTax greater than zero | An invoice can carry a TaxLine with no TotalTax key at all. |
Any TxnTaxDetail.TaxLine[].Amount greater than zero | Intuit's non-US automated-sales-tax sample pairs a TaxLine of 3.33 with TotalTax: 0 (in the request; its sample response corrects TotalTax). A record that ever shows that pair must not read as untaxed. |
Any stored line whose SalesItemLineDetail.TaxCodeRef.value is not NON | Catches an invoice carrying tax-coded lines and no TxnTaxDetail. |
The line-code rule reads the code's value, never the mere presence of
TaxCodeRef: QuickBooks stamps NON on non-taxable lines, so keying on
presence would treat every untaxed invoice as taxed. NON is compared
case-insensitively, and it is the only value that means "no tax" — US companies
get the TAX/NON pseudo-codes, non-US ones an opaque tax-code Id, so any
other value has to read as a real code.
A TxnTaxDetail that is present but cannot be parsed into the above — not an
object, a non-numeric TotalTax, a TaxLine that is not an array, a TaxLine
entry with no readable Amount, a TaxCodeRef with no readable value — is
also rejected, with an error saying the field could not be read. Ambiguity
fails closed: the state at risk lives on the server, so a field the action
cannot parse is not evidence that there is no tax.
GlobalTaxCalculation is deliberately not a signal. Intuit documents it as
required for every non-US company and documents no value that means "no tax"
(NotApplicable's trigger conditions are never explained and it appears in no
sample), so keying on it would refuse every non-US line replacement whether or
not the invoice is taxed. The shapes it would have caught are already covered:
a TaxExcluded invoice with tax-coded lines by the line-code rule, a
TaxInclusive one by its TaxLine.
What still goes through unchanged: an invoice with no TxnTaxDetail and no
coded lines; one whose TotalTax is 0 with no positive TaxLine and every
line coded NON; and any update that supplies full tax input.
An update with no lines is unaffected — a sparse body leaves Line alone, so
the stored tax is not at risk and a metadata-only edit still goes through.
The same five rules govern the update path of
upsertInvoiceByDocNumber, which also replaces the
whole Line array — on both of its branches, whether the target was resolved
from the caller's stored invoiceId or found by the DocNumber probe. Its create
path has no stored tax to protect and is not affected.
The TotalTax returned by the update is checked against the submitted one, to
the cent, exactly as on createInvoice. Supplying none of the tax or address
fields against an untaxed invoice leaves the request byte-identical to what
this action sent before they existed.
listInvoices
List invoices with optional filters.
{
"action": "quickbooks.listInvoices",
"config": {
"customerId": "{{input.customerId}}",
"balance": 0,
"balanceOperator": ">",
"txnDateAfter": "2024-01-01",
"limit": 100
}
}findInvoiceByDocNumber
Look up an invoice by its DocNumber (the invoice number / reference no.). Returns a normalized result rather than the raw QueryResponse, because QuickBooks omits the entity array entirely when nothing matches.
{
"action": "quickbooks.findInvoiceByDocNumber",
"config": {
"realmId": "{{input.realmId}}",
"docNumber": "5314-5050 BL"
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
realmId | string | Yes | QuickBooks company (realm) ID |
docNumber | string | Yes | Invoice number to look up |
Output:
{
"found": true,
"id": "239",
"syncToken": "0",
"entity": { "...": "the matching Invoice object, or null when found is false" }
}A zero-match lookup returns { "found": false, "id": null, "syncToken": null, "entity": null }.
upsertInvoiceByDocNumber
Create-or-update an invoice keyed on its DocNumber. The DocNumber probe runs first, so a rerun over the same period updates the invoice a previous attempt created instead of creating a duplicate — even when the caller no longer has the stored QuickBooks id. QuickBooks replaces the whole Line array on update, so every line must be supplied on every run.
{
"id": "upsert-payroll-invoice",
"type": "action",
"action": "quickbooks.upsertInvoiceByDocNumber",
"config": {
"realmId": "{{input.realmId}}",
"docNumber": "5314-5050 BL",
"customerId": "{{steps.find-customer.output.id}}",
"lines": [
{
"description": "Gross Payroll",
"amount": 2496.27,
"quantity": 1,
"unitPrice": 2496.27,
"itemId": "12"
}
],
"invoiceDate": "2026-07-15",
"dueDate": "2026-08-14",
"privateNote": "Payroll invoice for pay day 2026-07-15"
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
realmId | string | Yes | QuickBooks company (realm) ID |
docNumber | string | Yes | Invoice number — the idempotency key |
customerId | string | Yes | Customer ID |
lines | array | Yes | Invoice lines (at least one) |
lines[].description | string | No | Line description |
lines[].amount | number | Yes | Line amount |
lines[].quantity | number | No | Quantity (default: 1) |
lines[].unitPrice | number | No | Unit price (default: the line amount) |
lines[].itemId | string | No | Product/Service ID |
lines[].taxCodeId | string | No | Tax code (TAX, NON, or a tax-code id) → SalesItemLineDetail.TaxCodeRef; requires txnTaxDetail |
invoiceDate | string | No | Transaction date, YYYY-MM-DD (default: today) |
dueDate | string | No | Due date (YYYY-MM-DD) |
customerMemo | string | No | Memo for customer |
privateNote | string | No | Internal note |
txnTaxDetail | object | No | { totalTax, txnTaxCodeId } — both required → TxnTaxDetail; suppresses US Automated Sales Tax (see createInvoice) |
shipAddr | object | No | { line1, line2?, city?, countrySubDivisionCode?, postalCode?, country? } → ShipAddr |
billAddr | object | No | Same shape → BillAddr |
The three tax/address fields are sent on both the create and the update path. The four input-shape rejections listed under createInvoice run before the DocNumber probe, so bad input never costs a query, and on both paths the TotalTax returned by the write is checked against the submitted one, to the cent.
One further rejection is specific to the update path and necessarily runs after the probe: because this action always replaces the whole Line array, an update onto an invoice whose stored TxnTaxDetail.TotalTax is greater than zero is refused when no txnTaxDetail is supplied — the replacement lines would otherwise drop the tax and let Automated Sales Tax recompute it against a 200. The error names the stored tax; resend txnTaxDetail and a per-line taxCodeId recomputed for the new lines. The create path has no stored tax and is unaffected, as is an invoice stored with zero tax (a NON-coded line is not treated as taxed). See updateInvoice for the full rule set. That update path has still not been exercised against a live Automated Sales Tax company — see the note under createInvoice.
Output:
{
"operation": "created",
"invoiceId": "238",
"syncToken": "0",
"totalAmount": 100.0,
"docNumber": "5314-5050 BL",
"realmId": "9341454792746027",
"Invoice": { "...": "the QuickBooks Invoice object from the write response" }
}operation is "created" or "updated". syncToken and totalAmount are null when the response omits them.
Journal Entry Actions
A journal entry needs at least one debit line and one credit line, and the two columns must total the same amount. Each line names an account either by accountId (the QuickBooks Account Id) or by accountNumber (the account's number in the chart of accounts) — account numbers are resolved with a single chart-of-accounts query per call, and an unresolvable number fails the step.
createJournalEntry
Post a new journal entry. Returns the raw QuickBooks response.
{
"id": "post-payroll-je",
"type": "action",
"action": "quickbooks.createJournalEntry",
"config": {
"realmId": "{{input.realmId}}",
"docNumber": "5314-5050 BL",
"txnDate": "2026-07-15",
"privateNote": "Net payroll costs for pay day 2026-07-15",
"journalLines": [
{
"accountNumber": "400001",
"postingType": "debit",
"amount": 1222.92,
"description": "Net Payroll Costs"
},
{
"accountId": "44",
"postingType": "credit",
"amount": 1222.92,
"description": "Payroll Clearing"
}
]
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
realmId | string | Yes | QuickBooks company (realm) ID |
journalLines | array | Yes | Debit/credit distribution lines (at least one) |
journalLines[].amount | number | Yes | Line amount |
journalLines[].postingType | string | No | debit or credit — anything other than credit posts as a debit |
journalLines[].accountId | string | No | QuickBooks Account Id (takes precedence) |
journalLines[].accountNumber | string | No | Account number, resolved against the chart of accounts when accountId is absent |
journalLines[].description | string | No | Line description |
docNumber | string | No | Reference number for the entry |
txnDate | string | No | Transaction date, YYYY-MM-DD (default: today) |
privateNote | string | No | Internal note |
The executor also accepts lines as an alias for journalLines, so hand-authored workflow JSON can use the same key as the invoice actions.
Output:
{
"time": "2015-06-29T12:45:32.183-07:00",
"JournalEntry": { "...": "the created QuickBooks JournalEntry object" }
}findJournalEntryByDocNumber
Look up a journal entry by its DocNumber. Normalized the same way as findInvoiceByDocNumber.
{
"action": "quickbooks.findJournalEntryByDocNumber",
"config": {
"realmId": "{{input.realmId}}",
"docNumber": "5314-5050 BL"
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
realmId | string | Yes | QuickBooks company (realm) ID |
docNumber | string | Yes | Journal entry reference number to look up |
Output:
{
"found": true,
"id": "227",
"syncToken": "1",
"entity": { "...": "the matching JournalEntry object, or null when found is false" }
}upsertJournalEntryByDocNumber
Create-or-update a journal entry keyed on its DocNumber. Idempotent by construction: the DocNumber probe runs first, so a retried step updates the entry a previous attempt created instead of posting a duplicate. Lines are always sent in full because QuickBooks replaces the Line array wholesale on update.
{
"id": "upsert-payroll-je",
"type": "action",
"action": "quickbooks.upsertJournalEntryByDocNumber",
"config": {
"realmId": "{{input.realmId}}",
"docNumber": "5314-5050 BL",
"txnDate": "2026-07-15",
"privateNote": "Net payroll costs for pay day 2026-07-15",
"journalLines": [
{
"accountNumber": "400001",
"postingType": "debit",
"amount": 1222.92,
"description": "Net Payroll Costs"
},
{
"accountId": "44",
"postingType": "credit",
"amount": 1222.92,
"description": "Payroll Clearing"
}
]
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
realmId | string | Yes | QuickBooks company (realm) ID |
docNumber | string | Yes | Reference number — the idempotency key |
journalLines | array | Yes | Debit/credit distribution lines (at least one) |
journalLines[].amount | number | Yes | Line amount |
journalLines[].postingType | string | No | debit or credit — anything other than credit posts as a debit |
journalLines[].accountId | string | No | QuickBooks Account Id (takes precedence) |
journalLines[].accountNumber | string | No | Account number, resolved against the chart of accounts when accountId is absent |
journalLines[].description | string | No | Line description |
txnDate | string | No | Transaction date, YYYY-MM-DD (default: today) |
privateNote | string | No | Internal note |
Output:
{
"operation": "created",
"journalEntryId": "228",
"syncToken": "0",
"docNumber": "5314-5050 BL",
"realmId": "9341454792746027",
"JournalEntry": { "...": "the QuickBooks JournalEntry object from the write response" }
}operation is "created" or "updated". syncToken is null when the response omits it.
Payment Actions
createPayment
Record a customer payment. invoiceIds accepts one invoice; the full payment amount is allocated to it. For multiple invoices, use Batch with a Payment create operation and explicit Line amounts for each allocation.
{
"id": "record-payment",
"type": "action",
"action": "quickbooks.createPayment",
"config": {
"customerId": "{{input.customerId}}",
"amount": 1500.00,
"paymentDate": "2024-01-20",
"paymentMethodId": "1",
"invoiceIds": ["{{input.invoiceId}}"],
"referenceNumber": "CHK-12345"
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Customer ID |
amount | number | Yes | Payment amount |
paymentDate | string | No | Payment date (default: today) |
paymentMethodId | string | No | Payment method reference |
depositAccountId | string | No | Account to deposit to |
invoiceIds | array | No | Invoices to apply payment to |
referenceNumber | string | No | Check/reference number |
getPayment
Get a payment by ID.
{
"action": "quickbooks.getPayment",
"config": {
"paymentId": "{{input.paymentId}}"
}
}voidPayment
Void a payment.
{
"action": "quickbooks.voidPayment",
"config": {
"paymentId": "{{input.paymentId}}"
}
}Bill (AP) Actions
createBill
Create a vendor bill (accounts payable).
{
"id": "create-bill",
"type": "action",
"action": "quickbooks.createBill",
"config": {
"vendorId": "{{input.vendorId}}",
"lines": [
{
"description": "Office Supplies",
"amount": 250.00,
"accountId": "54",
"billableStatus": "NotBillable"
}
],
"dueDate": "2024-02-28",
"billNumber": "BILL-001"
}
}getBill / updateBill / deleteBill
{
"action": "quickbooks.getBill",
"config": { "billId": "{{input.billId}}" }
}Vendor Actions
createVendor
Create a new vendor.
{
"action": "quickbooks.createVendor",
"config": {
"displayName": "{{input.vendorName}}",
"companyName": "{{input.companyName}}",
"email": "{{input.email}}",
"is1099": true,
"taxId": "{{input.taxId}}"
}
}Account Actions
getAccountList
Get the chart of accounts.
{
"action": "quickbooks.getAccountList",
"config": {
"active": true,
"accountType": "Expense",
"classification": "Expense"
}
}Account Types: Bank, Accounts Receivable, Other Current Asset, Fixed Asset, Accounts Payable, Credit Card, Other Current Liability, Long Term Liability, Equity, Income, Cost of Goods Sold, Expense, Other Income, Other Expense
createAccount
Create a new account in the chart of accounts.
{
"action": "quickbooks.createAccount",
"config": {
"name": "Software Subscriptions",
"accountType": "Expense",
"accountSubType": "SubscriptionFees",
"description": "SaaS and software subscriptions"
}
}Journal Entry Actions
Journal entries post directly to the general ledger. Every line carries an account, an amount, and a posting side; total debits must equal total credits. The executors validate that balance before calling QuickBooks, so an unbalanced entry fails with an explicit message instead of a generic QuickBooks fault.
accountId is the QuickBooks internal account Id, not the account number.
Resolve numbers to Ids first with getAccountList or
query (SELECT Id, Name, AcctNum FROM Account) — Ids differ between
QuickBooks companies.
createJournalEntry
Create a journal entry.
{
"id": "post-journal-entry",
"type": "action",
"action": "quickbooks.createJournalEntry",
"config": {
"txnDate": "2026-07-17",
"docNumber": "5322-5217 JE",
"privateNote": "Payroll passthrough",
"lines": [
{
"accountId": "92",
"amount": 1222.92,
"postingType": "Debit",
"description": "Net payroll costs"
},
{
"accountId": "33",
"amount": 1222.92,
"postingType": "Credit",
"description": "Payroll clearing"
}
]
}
}Line fields: accountId (required), amount (required, non-negative),
postingType (required, Debit or Credit), description, entityType
(Customer / Vendor / Employee) with entityId, classId, departmentId.
Limits: at least two lines, with at least one debit and one credit;
docNumber is capped at 21 characters by QuickBooks; the balance check
tolerates rounding differences up to $0.005. Omit txnDate to let QuickBooks
date the entry with the company's own today — passing the caller's date is only
correct if it is already the intended accounting day.
Accounts Receivable / Accounts Payable lines require an entity. QuickBooks
rejects a journal entry line posting to an AR account without a Customer, or
to an AP account without a Vendor, with a generic business-validation fault —
a common cause of a failed create. Set entityType and entityId on those
lines.
getJournalEntry
Fetch a journal entry by its QuickBooks Id.
{
"action": "quickbooks.getJournalEntry",
"config": {
"journalEntryId": "{{steps.post-journal-entry.JournalEntry.Id}}"
}
}updateJournalEntry
Update a journal entry. The current SyncToken is read automatically before
the write. Omitted fields are preserved; supplying lines replaces the entry's
lines wholesale (QuickBooks has no per-line patch) and re-validates the balance.
{
"action": "quickbooks.updateJournalEntry",
"config": {
"journalEntryId": "148",
"privateNote": "Corrected workers comp margin",
"lines": [
{ "accountId": "92", "amount": 100.0, "postingType": "Debit" },
{ "accountId": "33", "amount": 100.0, "postingType": "Credit" }
]
}
}deleteJournalEntry
Delete a journal entry. QuickBooks offers no void operation for journal entries — delete is the only reversal.
{
"action": "quickbooks.deleteJournalEntry",
"config": {
"journalEntryId": "148"
}
}listJournalEntries
List journal entries, optionally filtered by document number or transaction date range.
{
"action": "quickbooks.listJournalEntries",
"config": {
"txnDateAfter": "2026-07-01",
"txnDateBefore": "2026-07-31",
"limit": 100
}
}findJournalEntryByDocNumber
Check whether a journal entry with a given document number already exists —
the duplicate guard to run before posting. Returns
{ exists, count, journalEntries } rather than the raw QuickBooks envelope, so
a condition step can branch on exists directly.
{
"action": "quickbooks.findJournalEntryByDocNumber",
"config": {
"docNumber": "5322-5217 JE"
}
}Report Actions
runReport
Run a financial report.
{
"id": "monthly-pnl",
"type": "action",
"action": "quickbooks.runReport",
"config": {
"reportType": "ProfitAndLoss",
"startDate": "2024-01-01",
"endDate": "2024-01-31",
"accountingMethod": "Accrual",
"summarizeBy": "Month"
}
}The searchable Report selector supports these Intuit endpoint names:
AccountList, AgedPayableDetail, AgedPayables, AgedReceivableDetail, AgedReceivables, BalanceSheet, CashFlow, CustomerBalance, CustomerBalanceDetail, CustomerIncome, GeneralLedger, InventoryValuationDetail, InventoryValuationSummary, JournalReport, ProfitAndLoss, ProfitAndLossDetail, ClassSales, CustomerSales, DepartmentSales, ItemSales, TaxSummary, TransactionList, TransactionListByVendor, TransactionListByCustomer, TransactionListWithSplits, TrialBalanceFR, TrialBalance, VendorBalance, VendorBalanceDetail, VendorExpenses.
Use reportParams for report-specific scalar query parameters, such as {"account":"1,2","summarize_column_by":"Month"}. These override the equivalent dedicated fields. Unsupported filters and reports return Intuit's error for that company.
The response preserves Header, Columns, and Rows, including nested sections and summaries. To analyze the report, add an Agent or Code step after monthly-pnl and reference {{steps.monthly-pnl.output.Header}}, {{steps.monthly-pnl.output.Columns}}, and {{steps.monthly-pnl.output.Rows}}. Keep column metadata with the rows so the analysis can distinguish labels, periods, and amounts. Chat can create the same workflow, for example: "Run January's Profit and Loss report in QuickBooks, then analyze expenses using the report's columns and rows."
Company Actions
getCompanyInfo
Read the connected company's profile. Use it as a pre-flight guard: a workflow can assert, before writing anything, that the (connection, realmId) pair it was handed really is the company it expects.
{
"id": "assert-company",
"type": "action",
"action": "quickbooks.getCompanyInfo",
"config": {
"realmId": "{{input.realmId}}"
}
}Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
realmId | string | Yes | QuickBooks company (realm) ID |
Output:
{
"realmId": "9341454792746027",
"companyName": "Larry's Bakery",
"legalName": "Larry's Bakery",
"CompanyInfo": { "...": "the full QuickBooks CompanyInfo object" }
}companyName and legalName are null when the response omits them.
Query Action
query
Execute a custom QuickBooks query.
{
"action": "quickbooks.query",
"config": {
"query": "SELECT * FROM Invoice WHERE Balance > '0' ORDER BY TxnDate DESC MAXRESULTS 50"
}
}Webhook Triggers
QuickBooks webhooks can trigger workflows on data changes.
{
"trigger": {
"type": "webhook",
"provider": "quickbooks",
"events": ["Invoice", "Customer", "Payment"]
}
}Event Types: Customer, Invoice, Payment, Bill, Vendor, Account, Purchase, SalesReceipt, Estimate
Example Workflow
Complete invoice workflow triggered by Stripe payment:
{
"name": "Stripe to QuickBooks Invoice",
"trigger": {
"type": "webhook",
"provider": "stripe",
"events": ["invoice.paid"]
},
"steps": [
{
"id": "find-customer",
"type": "action",
"action": "quickbooks.findCustomerByEmail",
"config": {
"email": "{{input.data.object.customer_email}}"
}
},
{
"id": "check-customer",
"type": "condition",
"config": {
"conditions": {
"left": "{{steps.find-customer.output.QueryResponse.Customer}}",
"operator": "exists"
},
"then": ["create-invoice"],
"else": ["create-customer"]
}
},
{
"id": "create-customer",
"type": "action",
"action": "quickbooks.createCustomer",
"config": {
"displayName": "{{input.data.object.customer_name}}",
"email": "{{input.data.object.customer_email}}"
}
},
{
"id": "create-invoice",
"type": "action",
"action": "quickbooks.createInvoice",
"config": {
"customerId": "{{steps.find-customer.output.QueryResponse.Customer[0].Id || steps.create-customer.output.Customer.Id}}",
"lines": [
{
"description": "{{input.data.object.lines.data[0].description}}",
"amount": "{{input.data.object.amount_paid / 100}}"
}
]
}
},
{
"id": "record-payment",
"type": "action",
"action": "quickbooks.createPayment",
"config": {
"customerId": "{{steps.find-customer.output.QueryResponse.Customer[0].Id || steps.create-customer.output.Customer.Id}}",
"amount": "{{input.data.object.amount_paid / 100}}",
"invoiceIds": ["{{steps.create-invoice.output.Invoice.Id}}"],
"referenceNumber": "{{input.data.object.id}}"
}
}
]
}Rate Limits
| Limit | Value |
|---|---|
| API calls per minute | 500 |
| Batch operations | 30 records per batch |
| Query results | 1000 records max |
| Concurrent connections | 10 |
Troubleshooting
Common Errors
| Error | Cause | Solution |
|---|---|---|
realmId is required | Missing company ID | Set config.realmId on the step (the block's Company (Realm) ID field) |
SyncToken mismatch | Concurrent updates | Refetch record before update |
Business validation error | Invalid data | Check field formats and requirements |
Throttled | Rate limit exceeded | Implement exponential backoff |
Configuration: realmId
Every QuickBooks action needs realmId (the company ID) to build its request path, and step config is the standard way to supply it. Set config.realmId on every QuickBooks step:
{
"action": "quickbooks.getCompanyInfo",
"config": {
"realmId": "9341454792746027"
}
}On the canvas this is the Company (Realm) ID field on the QuickBooks block, which is required. A step without it fails with realmId is required for QuickBooks API calls.
The action resolver also accepts trusted variables.realmId as a fallback for runtime callers. The canvas does not automatically populate this value from the selected connection; set the Company (Realm) ID explicitly when authoring a workflow.
Loopfour