Skip to content

Reference

QuickBooks

Set up QuickBooks OAuth for accounting and financial management

Connect your users to QuickBooks for accounting data, invoicing, expense tracking, and financial reporting.

PropertyValue
Provider IDquickbooks
CategoryFinance
PKCESupported (S256 — Alter sends it; Intuit accepts it but does not document PKCE)
Token refreshAutomatic
Redirect URIShown in Developer Portal

Go to the Intuit Developer Dashboard

Navigate to developer.intuit.com/app/developer/dashboard and sign in (or select My Hub > App dashboard).

Create a new app

Select the app card with a + and follow the on-screen steps. When asked for scopes, select Accounting (add Payments if the application needs the Payments API).

Configure the redirect URI

Open the app, select Settings in the left navigation, then the Redirect URIs tab. Select Add URI and paste the Alter callback URL from the Developer Portal. Add it under both Development and Production if both key sets will be used.

Get credentials

Select Keys and credentials, choose Development (sandbox companies only) or Production, turn on Show credentials, and copy the Client ID and Client Secret. Production credentials appear only after Intuit approves the Production Key questionnaire.

Open the Developer Portal

Go to portal.alterauth.com and navigate to the application.

Add QuickBooks provider

Go to OAuth Providers > Add Provider > QuickBooks.

Enter credentials

  • Client ID: Paste your QuickBooks Client ID
  • Client Secret: Paste your QuickBooks Client Secret

Select scopes

Choose the scopes the application needs.

Save

Click Save. The provider is now active.

The same configuration from a terminal, with the client secret read from a file so it never enters shell history:

Terminal window
alter providers create --provider quickbooks \
--scopes com.intuit.quickbooks.accounting \
--client-id <client-id> --client-secret @quickbooks-secret.txt \
--redirect-uri <alter-callback-url>

Intuit runs one OAuth server for both environments. What makes a connection a sandbox connection is the key set and the company, not an Alter setting:

  • Development keys connect only sandbox companies. Configure the provider with the Development Client ID and Secret during development.
  • Production keys connect real companies. Update the provider with the Production keys when going live; existing connections then need fresh consent.

Do not pass --environment sandbox for QuickBooks. That flag selects a separate provider deployment and is accepted only for providers that publish one (available_environments in alter providers list-catalog --output json); QuickBooks publishes only production, so the CLI rejects it before saving.

The API host does differ by environment. Alter records the host that confirmed the company and returns it on the grant as api_base_url / apiBaseUrl:

Companyapi_base_url
Production companyhttps://quickbooks.api.intuit.com
Sandbox companyhttps://sandbox-quickbooks.api.intuit.com

Build Accounting URLs from that value instead of a hardcoded or configured host. Operators see the same value as API host in the developer portal’s grant details and in the API HOST column of alter grants list. The Payments API uses its own hosts (https://api.intuit.com in production, https://sandbox.api.intuit.com for a sandbox company); choose the one that matches the grant’s api_base_url.

ScopeDescription
com.intuit.quickbooks.accountingAccess to QuickBooks accounting data
com.intuit.quickbooks.paymentAccess to QuickBooks payment data
openidAccess to OpenID Connect identity
profileAccess to user profile information
emailAccess to user email address

Every QuickBooks Online Accounting URL carries the connected company’s id — Intuit calls it the realmId — as in /v3/company/{realmId}/preferences. Intuit reliably returns it only on the OAuth redirect (its OpenID ID-token claim is optional and populated only when the user is signed in to QuickBooks), so Alter captures it at connect time, checks with the newly issued token that the company’s CompanyInfo is readable, and exposes it on the grant:

Grant field (Python / TypeScript)Value
account_identifier / accountIdentifierThe realmId, as Intuit sent it (a numeric string)
account_display_name / accountDisplayNameThe company name
api_base_url / apiBaseUrlThe company’s Accounting API host (production or sandbox)

Both are the grant’s general-purpose account fields, not QuickBooks-specific ones: the same account_identifier holds an email address for most providers and an account id for others, so read it per provider rather than assuming a shape. See the Python and TypeScript type reference. Either field can be null — see below.

Build API URLs from the grant. Right after the Connect flow, the same host is on the returned ConnectResult as api_base_url / apiBaseUrl (and the company id as account_identifier / accountIdentifier), so code that has just connected the company does not need to list grants; code that runs later reads them from list_grants() / listGrants():

page = await app.list_grants(end_user_token=end_user_token, provider_id="quickbooks")
# Rows are a union of connection kinds; keep the OAuth ones.
grants = [g for g in page.grants if g.grant_kind == "oauth"]
if not grants:
raise RuntimeError("No QuickBooks connection yet — run the Connect flow first.")
qb = grants[0]
realm_id = qb.account_identifier
api_base = qb.api_base_url # production or sandbox host for this company
if realm_id is None or api_base is None:
# Not an error state: see "when the company id is null" below.
raise RuntimeError("Company not confirmed — ask the user to reconnect.")
response = await app.request(
"GET",
f"{api_base}/v3/company/{realm_id}/preferences",
grant_id=qb.grant_id,
)
const page = await app.listGrants({ endUserToken, providerId: "quickbooks" });
// Rows are a union of connection kinds; keep the OAuth ones.
const qb = page.grants.find((g) => g.grantKind === "oauth");
if (!qb || qb.grantKind !== "oauth") {
throw new Error("No QuickBooks connection yet — run the Connect flow first.");
}
const realmId = qb.accountIdentifier;
const apiBase = qb.apiBaseUrl; // production or sandbox host for this company
if (realmId === null || apiBase === null) {
// Not an error state: see "when the company id is null" below.
throw new Error("Company not confirmed — ask the user to reconnect.");
}
const response = await app.request(
"GET",
`${apiBase}/v3/company/${realmId}/preferences`,
{ grantId: qb.grantId },
);

The company id is confirmed through the Accounting API, so the com.intuit.quickbooks.accounting scope is always requested and cannot be removed from a connection.

A null company id means Alter could not confirm the company identity when the connection was established. The token can still be active: connection health and company identification are separate facts.

This can affect older connections or connections whose CompanyInfo confirmation failed. After confirmation is available, reconnect from the wallet to populate the verified company id on the existing connection. A fresh Connect flow can create a separate credential when the old credential has no verified subject.

If another active credential already owns the same deduplication subject, wallet reconnect preserves that ownership instead of merging credentials. It can still refresh the original connection’s token, company id and name; only the colliding internal subject backfill is skipped.

Treat account_identifier as nullable at every call site. Show a reconnect prompt when it is null instead of building an Accounting URL from it. Connect polling and browser callbacks return null, never a manufactured "Unknown" identifier.

Sandbox (Development-key) companies are confirmed against the sandbox Accounting host. Alter tries its production and sandbox candidates, uses the first valid authenticated CompanyInfo response, and records that host as the grant’s api_base_url. Connections made before Alter recorded the host have a null api_base_url; reconnect them from the wallet to capture it.

The identifier is the verified callback realmId. CompanyInfo.Id is the CompanyInfo object’s ID (Intuit’s example is "1"), not the company realm. To read CompanyInfo yourself, Intuit’s path repeats the realm — /v3/company/{realmId}/companyinfo/{realmId} — so the {companyInfoId} placeholder in the operations table below is also the realmId. The optional company display name comes from CompanyInfo.CompanyName. See the CompanyInfo reference.

Wrong-company requests and connection health

Section titled “Wrong-company requests and connection health”

Use the verified company realm in the Accounting URL. A provider 401 for a different realm is returned as the failed API call; it does not mark the shared connection as needing reconnect. A rejection for the connection’s own realm still triggers token refresh or reconnect handling.

  • QuickBooks has separate sandbox and production environments. Use sandbox (Development) credentials during development and read the API host from the grant’s api_base_url (see Sandbox and production).
  • The com.intuit.quickbooks.accounting scope is required for every Accounting API call; Alter always requests it (see Company identity above).
  • See the QuickBooks OAuth 2.0 documentation for more details.

Alter policy rules can target these attested operations and families for operation-level and parameter-aware controls.

Operation IDFamiliesMethodProvider path
accounts/createwritePOST/v3/company/{realmId}/account
accounts/getreadGET/v3/company/{realmId}/account/{accountId}
batchdelete, payment, read, writePOST/v3/company/{realmId}/batch
bills/createwritePOST/v3/company/{realmId}/bill
bills/getreadGET/v3/company/{realmId}/bill/{billId}
companyinfo/getreadGET/v3/company/{realmId}/companyinfo/{companyInfoId}
customers/createwritePOST/v3/company/{realmId}/customer
customers/getreadGET/v3/company/{realmId}/customer/{customerId}
invoices/createwritePOST/v3/company/{realmId}/invoice
invoices/getreadGET/v3/company/{realmId}/invoice/{invoiceId}
invoices/sendsendPOST/v3/company/{realmId}/invoice/{invoiceId}/send
items/createwritePOST/v3/company/{realmId}/item
items/getreadGET/v3/company/{realmId}/item/{itemId}
payments/createpayment, writePOST/v3/company/{realmId}/payment
payments/getreadGET/v3/company/{realmId}/payment/{paymentId}
preferences/getreadGET/v3/company/{realmId}/preferences
queryreadGET/v3/company/{realmId}/query
reports/balance-sheetreadGET/v3/company/{realmId}/reports/BalanceSheet
reports/profit-and-lossreadGET/v3/company/{realmId}/reports/ProfitAndLoss
vendors/createwritePOST/v3/company/{realmId}/vendor
vendors/getreadGET/v3/company/{realmId}/vendor/{vendorId}

Report an issue with this page

Necessary

Required for sign-in, security, authorization, and remembering your choices.

Always active

Analytics

Helps us understand which product and documentation features are useful.

Performance diagnostics

Uses performance tracing and privacy-masked session replay to diagnose problems.

You can change these choices at any time from Cookie settings.