Loopfour
IntegrationsAccounting

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

ScopeDescription
com.intuit.quickbooks.accountingFull 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:

FieldTypeRequiredDescription
displayNamestringYesDisplay name (unique identifier)
firstNamestringNoFirst name
lastNamestringNoLast name
companyNamestringNoCompany name
emailstringNoPrimary email address
phonestringNoPhone number
mobilestringNoMobile number
billingAddressobjectNoBilling address
notesstringNoInternal notes
activebooleanNoActive 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:

FieldTypeRequiredDescription
customerIdstringYesCustomer ID
linesarrayYesLine items (at least one)
lines[].descriptionstringNoLine item description
lines[].amountnumberYesLine amount
lines[].quantitynumberNoQuantity (default: 1)
lines[].unitPricenumberNoUnit price
lines[].itemIdstringNoProduct/Service ID
dueDatestringNoDue date (YYYY-MM-DD)
invoiceDatestringNoInvoice date (default: today)
invoiceNumberstringNoCustom invoice number
customerMemostringNoMemo for customer
privateNotestringNoInternal note
emailstringNoEmail 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:

FieldTypeRequiredDescription
realmIdstringYesQuickBooks company (realm) ID
docNumberstringYesInvoice 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:

FieldTypeRequiredDescription
realmIdstringYesQuickBooks company (realm) ID
docNumberstringYesInvoice number — the idempotency key
customerIdstringYesCustomer ID
linesarrayYesInvoice lines (at least one)
lines[].descriptionstringNoLine description
lines[].amountnumberYesLine amount
lines[].quantitynumberNoQuantity (default: 1)
lines[].unitPricenumberNoUnit price (default: the line amount)
lines[].itemIdstringNoProduct/Service ID
invoiceDatestringNoTransaction date, YYYY-MM-DD (default: today)
dueDatestringNoDue date (YYYY-MM-DD)
customerMemostringNoMemo for customer
privateNotestringNoInternal 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:

FieldTypeRequiredDescription
realmIdstringYesQuickBooks company (realm) ID
journalLinesarrayYesDebit/credit distribution lines (at least one)
journalLines[].amountnumberYesLine amount
journalLines[].postingTypestringNodebit or credit — anything other than credit posts as a debit
journalLines[].accountIdstringNoQuickBooks Account Id (takes precedence)
journalLines[].accountNumberstringNoAccount number, resolved against the chart of accounts when accountId is absent
journalLines[].descriptionstringNoLine description
docNumberstringNoReference number for the entry
txnDatestringNoTransaction date, YYYY-MM-DD (default: today)
privateNotestringNoInternal 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:

FieldTypeRequiredDescription
realmIdstringYesQuickBooks company (realm) ID
docNumberstringYesJournal 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:

FieldTypeRequiredDescription
realmIdstringYesQuickBooks company (realm) ID
docNumberstringYesReference number — the idempotency key
journalLinesarrayYesDebit/credit distribution lines (at least one)
journalLines[].amountnumberYesLine amount
journalLines[].postingTypestringNodebit or credit — anything other than credit posts as a debit
journalLines[].accountIdstringNoQuickBooks Account Id (takes precedence)
journalLines[].accountNumberstringNoAccount number, resolved against the chart of accounts when accountId is absent
journalLines[].descriptionstringNoLine description
txnDatestringNoTransaction date, YYYY-MM-DD (default: today)
privateNotestringNoInternal 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:

FieldTypeRequiredDescription
customerIdstringYesCustomer ID
amountnumberYesPayment amount
paymentDatestringNoPayment date (default: today)
paymentMethodIdstringNoPayment method reference
depositAccountIdstringNoAccount to deposit to
invoiceIdsarrayNoInvoices to apply payment to
referenceNumberstringNoCheck/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:

ReportDescription
ProfitAndLossIncome statement
BalanceSheetBalance sheet
CashFlowCash flow statement
TrialBalanceTrial balance
GeneralLedgerGeneral ledger detail
AgedReceivablesAR aging summary
AgedPayablesAP aging summary
CustomerIncomeRevenue by customer
VendorExpensesExpenses 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:

FieldTypeRequiredDescription
realmIdstringYesQuickBooks 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

LimitValue
API calls per minute500
Batch operations30 records per batch
Query results1000 records max
Concurrent connections10

Troubleshooting

Common Errors

ErrorCauseSolution
realmId is requiredMissing company IDSet config.realmId on the step (the block's Company (Realm) ID field)
SyncToken mismatchConcurrent updatesRefetch record before update
Business validation errorInvalid dataCheck field formats and requirements
ThrottledRate limit exceededImplement 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.

On this page