Integrate with QuickBooks Online
How to connect your invoice pipeline to QuickBooks Online: OAuth 2.0 flow, entity types, sync patterns, rate limits, and the certification path for listing on the Intuit AppStore.

QuickBooks Online is the default accounting system for small and mid-sized businesses in the US and Canada. That is not marketing language; it is a market share fact that shapes what integrations you need to build. Any tool that processes invoices, manages vendor bills, or handles expense tracking will eventually face the question: how does this get into QBO? This guide answers that question in full, from the first OAuth redirect to AppStore certification.
Why QBO is the default for US and CA small business
Intuit has around 7 million active QuickBooks Online subscribers globally, with the US and Canadian market making up the overwhelming majority. For a SaaS product targeting North American finance teams or bookkeepers, QBO compatibility is not a differentiator; it is table stakes.
The reasons for QBO's dominance are structural. First, the accountant channel: the majority of small business owners in the US and Canada rely on an accountant or bookkeeper who dictates software choice. Accountants standardize on QBO because it has a mature partner program (QuickBooks ProAdvisor), multi-company management tooling, and a large support community. When an accountant recommends a tool, they want it to work with QBO without friction.
Second, the App Ecosystem: the Intuit AppStore has hundreds of certified integrations across categories like inventory, payroll, time tracking, and document management. Users search the AppStore when they have a workflow gap. Being listed there generates qualified inbound traffic that no amount of content marketing fully replaces.
Third, the pricing and accessibility: QBO Simple Start starts under $20/month. At that price point, it is accessible to sole proprietors and early-stage startups, which means it often appears in a company's stack long before any serious accounting sophistication is required. Switching costs accumulate quickly once chart of accounts, vendor lists, and historical data are inside QBO. Most companies stay.
If your integration serves bookkeepers managing multiple client companies, the Canada market follows the same pattern. French-language QBO is available, the API is the same, and the Intuit AppStore serves Canadian businesses.
OAuth 2.0 authorization flow
QBO uses the standard OAuth 2.0 authorization code grant. Here is what the flow looks like in practice.
Step 1: Redirect to Intuit. Send the user to the Intuit authorization endpoint with your client_id, redirect_uri, response_type=code, scope, and a state parameter you generate (CSRF token). The accounting scope string is com.intuit.quickbooks.accounting. The authorization URL base is https://appcenter.intuit.com/connect/oauth2.
Step 2: User grants access. The user signs in to their Intuit account (or is already signed in), selects a company from their list, and approves the connection. Intuit redirects back to your redirect_uri with a short-lived code and the realmId (the company identifier you will use for all subsequent API calls).
Step 3: Exchange code for tokens. POST to https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer with the code, redirect_uri, grant_type=authorization_code, and your credentials (Basic auth with client ID and secret). The response contains an access_token (1-hour expiry) and a refresh_token (100-day expiry, renewed on each use).
Step 4: Store tokens and realmId securely. The realmId is your stable identifier for that QBO company. Store it alongside the encrypted tokens. Every API call includes the realmId in the URL path: https://quickbooks.api.intuit.com/v3/company/{realmId}/bill.
Step 5: Refresh tokens before expiry. The access token expires after 1 hour. Implement a refresh check before each API call: if the access token expires within the next 5 minutes, POST to the token endpoint with grant_type=refresh_token. The 100-day refresh token window resets on each successful refresh.
Two security points worth emphasizing. First, validate the state parameter when the authorization redirect returns to prevent CSRF attacks. Second, never store QBO tokens in plaintext; use encrypted storage (Supabase Vault or equivalent). See our integrations feature page for how Inbox Ledger handles OAuth token storage for connected accounting systems.
For sandbox testing, the authorization endpoint and base URL differ: use https://sandbox-quickbooks.api.intuit.com/v3/company/{realmId}/ instead of the production URL. Your credentials work in both environments; only the base URL changes.
Entity types: what you are actually creating
QBO's data model uses specific entity names that do not always match everyday language. Getting this right matters because creating the wrong entity type silently produces incorrect accounting.
Bill (accounts payable)
A Bill is what you create when you receive an invoice from a vendor and owe money. This is the entity most invoice-processing integrations write. Required fields: a VendorRef pointing to an existing Vendor, a TxnDate (transaction date), a DueDate, and at least one Line with an Amount and an AccountBasedExpenseLineDetail or ItemBasedExpenseLineDetail. The DocNumber field is where the vendor's invoice number goes.
{
"VendorRef": { "value": "56" },
"TxnDate": "2026-04-15",
"DueDate": "2026-05-15",
"DocNumber": "INV-2026-00342",
"Line": [
{
"Amount": 1250.0,
"DetailType": "AccountBasedExpenseLineDetail",
"AccountBasedExpenseLineDetail": {
"AccountRef": { "value": "7" }
}
}
]
}
Invoice (accounts receivable)
An Invoice in QBO is a billing document you send to your customers. Creating one records money owed to you. This is the opposite of what most document-capture integrations want. The entity name causes real confusion because people say "I got an invoice from Amazon" and assume they should create a QBO Invoice. They should not; they should create a Bill.
Only create QBO Invoices if your product generates outgoing billing documents for your users' customers.
Expense
An Expense records a payment that has already been made, either by credit card, check, or cash. Use this entity when the vendor document is a receipt showing payment rather than a bill with a future due date. The PaymentType field distinguishes Cash, Check, CreditCard. Like Bills, Expenses require a valid account reference on each line.
JournalEntry
A JournalEntry is a lower-level entity for manual debit/credit pairs. Most invoice integrations do not need it. It is useful for specific accounting adjustments, reclassifications, or when you need to post a transaction that does not fit neatly into Bill or Expense semantics. Requires accounting expertise to use correctly; a misconfigured JournalEntry can corrupt the trial balance without any obvious error from the API.
Common integration patterns
Three patterns cover the majority of QBO integrations. Which one you pick depends on how much control your users want over what lands in their books.
One-way sync (push to QBO)
The simplest pattern: your system is the source of truth for extracted invoice data, and you push Bills to QBO on a defined trigger (document approved, confidence threshold met, or manual user action). QBO is a downstream sink. No data flows back from QBO to your system.
This is the right starting point for most integrations. It is easy to reason about, easy to test, and avoids the complexity of conflict resolution. The main limitation is that changes made directly in QBO (edited amounts, different category assignment, manual voids) are invisible to your system. For most users, this is acceptable; they do corrections in QBO directly.
Implementation note: always check for an existing Bill with the same DocNumber and VendorRef before posting. See the duplicate prevention section below.
Two-way sync
Two-way sync adds a read pass from QBO back into your system. You periodically poll the QBO ChangeDataCapture endpoint (/v3/company/{realmId}/cdc?entities=Bill,Vendor&changedSince={timestamp}) to pick up modifications made directly in QBO. Changed entities are reflected in your system's records.
Two-way sync is significantly harder to build correctly. You need a conflict resolution strategy for cases where both systems modified the same record between sync cycles. The most common approach is "last write wins with a change log," but for financial data, teams often prefer an alert-and-manual-resolve pattern rather than silent overwrite.
Use two-way sync when your users expect changes made in QBO to be visible in your tool's dashboard. Skip it if your tool is purely a capture-and-post pipeline.
Minimal-touch posting
A middle path: your integration posts Bills to QBO but marks them as DocStatus: Payable and leaves payment, categorization review, and approval to the QBO user. Your system provides the structured data extraction; QBO and the accountant handle the review and payment workflow.
This pattern is popular with bookkeepers because it preserves their control over what hits the books. The integration does the extraction work; the human does the accounting judgment. It also limits the blast radius of extraction errors: a wrong amount gets caught before payment rather than after.
Vendor and Chart of Accounts management
Two lookups need to happen before you can post any Bill: find or create the Vendor, and identify the correct expense account.
Vendor lookup and creation
Every Bill requires a VendorRef.value containing the QBO internal ID of an existing Vendor. Query the full vendor list with:
GET /v3/company/{realmId}/query?query=SELECT * FROM Vendor MAXRESULTS 1000
Build a local map keyed on normalized display name. Normalization matters: strip trailing punctuation, collapse whitespace, lowercase. "Amazon.com, Inc." and "Amazon.com Inc" should map to the same record.
On a match, use the existing ID. On no match, create a new Vendor:
{ "DisplayName": "Acme Software Ltd" }
Refresh the vendor cache at the start of each sync session or daily, whichever is more frequent. Users add vendors directly in QBO between syncs and a stale cache will create duplicates.
Chart of Accounts mapping
Bills need an expense account on each line. Most integrations start with a default "catch-all" expense account (often "Uncategorized Expense" or a user-configured default) and let the accountant reassign in QBO. That is fine for getting started.
A more useful approach is AI-assisted category mapping: use the vendor name, line item descriptions, and document type to suggest the most likely expense account from the company's chart of accounts. Present the suggestion to the user for confirmation before the first time a given vendor posts, then remember it for subsequent documents from the same vendor. This is the "learning categorization" pattern that most mature AP automation tools implement.
Query the account list with:
SELECT * FROM Account WHERE AccountType = 'Expense' MAXRESULTS 1000
Store the account ID and name locally; the chart of accounts rarely changes, so refreshing weekly is sufficient.
Rate limits, sandbox, and webhook setup
Rate limits
QBO applies limits at the company (realm) level. The main limits:
- 500 requests per minute per realm. Exceeding returns HTTP 429 with a
Retry-Afterheader. Respect it; retrying immediately burns your budget and the next request will also fail. - 10 concurrent requests per realm. In-flight request limit, not a per-second rate. Relevant for async bulk operations.
- Batch API: up to 30 operations per batch request. One batch counts as one API call. For bulk imports, this is the tool to use. A batch of 30 Bill creates consumes 1 rate limit unit instead of 30.
For historical imports, a safe pattern is processing 250 to 300 requests per minute with an exponential backoff on 429s starting at 2 seconds. Never implement a fixed-delay retry; it coordinates poorly with QBO's variable Retry-After values.
Sandbox environment
Create a sandbox app in the Intuit developer portal at developer.intuit.com. The portal provisions a test company automatically. Use sandbox-quickbooks.api.intuit.com as your API base URL. The OAuth authorization endpoint is the same (appcenter.intuit.com), but you need to request sandbox access tokens from that endpoint.
Sandbox data persists across sessions but may be reset by Intuit periodically. Do not rely on specific sandbox entity IDs across development sessions; always query for the data you need rather than hardcoding IDs.
Webhook setup
Register a webhook notification URL in your app's developer portal settings under "Webhooks." QBO will POST a compact notification to that URL when entities change in connected realms. The payload structure:
{
"eventNotifications": [
{
"realmId": "1234567890",
"dataChangeEvent": {
"entities": [
{
"name": "Bill",
"id": "145",
"operation": "Update",
"lastUpdated": "2026-04-24T10:23:00"
}
]
}
}
]
}
Note: the payload tells you something changed, not what changed. You must query GET /v3/company/{realmId}/bill/{id} to retrieve the actual data.
Verify webhook authenticity using the HMAC-SHA256 signature sent in the intuit-signature header. Compute HMAC-SHA256(webhookVerifierToken, rawBody) and compare using a constant-time comparison. Reject any request where the signature does not match.
Start for free and extract your first 10 invoices without a credit card.
Certification and AppStore listing
AppStore listing requires passing Intuit's technical certification. It is a meaningful bar, not a rubber stamp. The review checks:
OAuth correctness. The reviewer walks through your connect flow, verifies scopes are minimal (no requesting accounting scope if you only need payment data), and checks that disconnect works cleanly. Intuit sends a RealmID disconnect webhook when a user revokes access from their QBO account. Your app must handle this event by removing the stored tokens and stopping any scheduled sync for that realm.
Error handling. The reviewer expects your app to surface meaningful errors when QBO calls fail, handle token expiry gracefully (auto-refresh rather than forcing re-auth), and not retry failed calls indefinitely.
Data integrity. You should not delete entities in QBO via the API without explicit user action. Voiding a Bill (setting SyncToken and calling the void endpoint) is acceptable; programmatic delete is not. The reviewer will check whether your integration can corrupt a company's books through unintended writes.
Security. Your app must use HTTPS, store tokens encrypted, and not log access tokens. The Intuit security questionnaire covers these points explicitly.
User experience. Your connect and disconnect flows must be clear. A user should be able to see which QBO company is connected and disconnect it without contacting support.
The AppStore review timeline is typically 2 to 4 weeks from submission. Budget time for one or two revision cycles; the reviewer often flags minor OAuth handling issues that require a fix and resubmission.
For internal tools or apps serving a single company, none of this is required. The API is accessible without certification. Certification is the gate for public distribution through Intuit's channel.
Pitfalls: what breaks in production
Duplicate Bills
The single most common production issue. If your integration posts a Bill without checking for an existing record with the same DocNumber and VendorRef, a retry after a timeout or a webhook re-delivery creates a duplicate Bill. QBO does not enforce uniqueness on DocNumber at the API level (it warns users in the UI but allows duplicates via API).
Prevention: before posting, query SELECT * FROM Bill WHERE DocNumber = '{docNumber}' AND VendorRef = '{vendorId}'. If a result exists with the same amount and date, skip the post and record the existing ID. If the amount differs, flag for human review rather than overwriting.
Category mapping drift
A vendor changes what they sell, or your user reorganizes their chart of accounts. Stored category mappings go stale. Bills post to the wrong account for months before anyone notices.
Mitigation: track the account name alongside the account ID in your local mapping. On each sync session, verify the stored account ID still exists in the chart of accounts. If it was deleted or renamed, surface a re-mapping prompt rather than posting to a nonexistent account (which will fail with a validation error and block the whole bill).
Closing date conflicts
QBO allows a company to set a closing date for their books. Transactions dated on or before the closing date require a password override or are hard-blocked, depending on company settings. Historical backfills hit this immediately when posting Bills for prior fiscal years.
Detection: query GET /v3/company/{realmId}/preferences. The BookCloseDate field in the response contains the closing date if set. Before posting a Bill, compare TxnDate against the closing date and warn the user if a conflict exists. Do not silently fail; a blocked Bill leaves a gap in the import with no indication to the user.
Token refresh race conditions
Multi-tenant integrations that process many companies in parallel can hit a race condition: two concurrent workers both detect an expiring token and both attempt to refresh it simultaneously. QBO invalidates the previous refresh token on each successful refresh, so the second refresh attempt fails.
Solution: use an atomic lock (database row lock or Redis lock) around the token refresh operation for each realm. Only one worker should refresh at a time; others wait and then read the freshly written token.
Pagination assumptions
QBO query results are paginated to 1000 records per request by default. Vendor lists, account lists, and historical Bill queries can exceed this. Always implement pagination using STARTPOSITION and MAXRESULTS until the returned count is less than the page size. A silent truncation at 1000 vendors is the kind of bug that only surfaces when a new vendor happens to fall alphabetically after the 1000th record.
QBO integration is not technically difficult, but it has enough surface area to produce subtle errors that compound over time. Duplicate Bills, stale mappings, and closed-book conflicts are all fixable but expensive to discover after months of silent misfires. Build the safeguards in from the first version.
If you want to see how Inbox Ledger handles the QBO sync for invoices that arrive by email, the email to QuickBooks pipeline post covers the full flow from inbox to posted Bill. For integrations on the Xero side, the Xero integration guide follows the same structure. Teams evaluating multiple accounting platforms should also check the Xero add-ons comparison and our QuickBooks portal page for vendor-specific setup details. For a broader view of invoice capture sources beyond email, see the Amazon Business portal page for platform-specific patterns, and our alternatives comparison hub for how different tools handle the QBO posting step.