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
QuickBooks Online is a cloud-based accounting platform for small to medium businesses. The integration supports:
- Customer Management - Create, update, and search customers
- Invoicing - Create, send, void, and track invoices
- Journal Entries - Post debit/credit distribution lines to the general ledger
- DocNumber Upserts - Create-or-update invoices and journal entries keyed on their reference number, so a rerun updates instead of duplicating
- Payments - Record and manage customer payments
- Bills (AP) - Create and manage vendor bills
- Vendors - Manage vendor records
- Chart of Accounts - Access and create accounts
- Journal Entries - Create, read, update, delete, and search general ledger entries
- Financial Reports - Run P&L, Balance Sheet, and more
- Company Info - Read the connected company profile as a pre-flight guard
- Custom Queries - Execute custom queries
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 |
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 |
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.
{
"action": "quickbooks.updateInvoice",
"config": {
"invoiceId": "{{input.invoiceId}}",
"dueDate": "2024-03-01",
"customerMemo": "Updated payment terms"
}
}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 |
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 |
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.
{
"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"
}
}Available Reports:
| Report | Description |
|---|---|
ProfitAndLoss | Income statement |
BalanceSheet | Balance sheet |
CashFlow | Cash flow statement |
TrialBalance | Trial balance |
GeneralLedger | General ledger detail |
AgedReceivables | AR aging summary |
AgedPayables | AP aging summary |
CustomerIncome | Revenue by customer |
VendorExpenses | Expenses by vendor |
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 only 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 runtime never derives realmId from the selected connection, and it is not populated from workflow variables — config.realmId is the value that is actually read.
Loopfour