Reference
QuickBooks
Set up QuickBooks OAuth for accounting and financial management
Overview
Section titled “Overview”Connect your users to QuickBooks for accounting data, invoicing, expense tracking, and financial reporting.
| Property | Value |
|---|---|
| Provider ID | quickbooks |
| Category | Finance |
| PKCE | Supported (S256 — Alter sends it; Intuit accepts it but does not document PKCE) |
| Token refresh | Automatic |
| Redirect URI | Shown in Developer Portal |
Step 1: Create a QuickBooks OAuth App
Section titled “Step 1: Create a QuickBooks OAuth App”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.
Step 2: Add to Alter Vault
Section titled “Step 2: Add to Alter Vault”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.
With the CLI
Section titled “With the CLI”The same configuration from a terminal, with the client secret read from a file so it never enters shell history:
alter providers create --provider quickbooks \ --scopes com.intuit.quickbooks.accounting \ --client-id <client-id> --client-secret @quickbooks-secret.txt \ --redirect-uri <alter-callback-url>Sandbox and production
Section titled “Sandbox and production”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:
| Company | api_base_url |
|---|---|
| Production company | https://quickbooks.api.intuit.com |
| Sandbox company | https://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.
Available Scopes
Section titled “Available Scopes”| Scope | Description |
|---|---|
com.intuit.quickbooks.accounting | Access to QuickBooks accounting data |
com.intuit.quickbooks.payment | Access to QuickBooks payment data |
openid | Access to OpenID Connect identity |
profile | Access to user profile information |
email | Access to user email address |
Company identity (realmId)
Section titled “Company identity (realmId)”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 / accountIdentifier | The realmId, as Intuit sent it (a numeric string) |
account_display_name / accountDisplayName | The company name |
api_base_url / apiBaseUrl | The 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_identifierapi_base = qb.api_base_url # production or sandbox host for this companyif 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 companyif (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.
When the company id is null
Section titled “When the company id is null”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.accountingscope 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.
Policy-cataloged operations
Section titled “Policy-cataloged operations”Alter policy rules can target these attested operations and families for operation-level and parameter-aware controls.
| Operation ID | Families | Method | Provider path |
|---|---|---|---|
accounts/create | write | POST | /v3/company/{realmId}/account |
accounts/get | read | GET | /v3/company/{realmId}/account/{accountId} |
batch | delete, payment, read, write | POST | /v3/company/{realmId}/batch |
bills/create | write | POST | /v3/company/{realmId}/bill |
bills/get | read | GET | /v3/company/{realmId}/bill/{billId} |
companyinfo/get | read | GET | /v3/company/{realmId}/companyinfo/{companyInfoId} |
customers/create | write | POST | /v3/company/{realmId}/customer |
customers/get | read | GET | /v3/company/{realmId}/customer/{customerId} |
invoices/create | write | POST | /v3/company/{realmId}/invoice |
invoices/get | read | GET | /v3/company/{realmId}/invoice/{invoiceId} |
invoices/send | send | POST | /v3/company/{realmId}/invoice/{invoiceId}/send |
items/create | write | POST | /v3/company/{realmId}/item |
items/get | read | GET | /v3/company/{realmId}/item/{itemId} |
payments/create | payment, write | POST | /v3/company/{realmId}/payment |
payments/get | read | GET | /v3/company/{realmId}/payment/{paymentId} |
preferences/get | read | GET | /v3/company/{realmId}/preferences |
query | read | GET | /v3/company/{realmId}/query |
reports/balance-sheet | read | GET | /v3/company/{realmId}/reports/BalanceSheet |
reports/profit-and-loss | read | GET | /v3/company/{realmId}/reports/ProfitAndLoss |
vendors/create | write | POST | /v3/company/{realmId}/vendor |
vendors/get | read | GET | /v3/company/{realmId}/vendor/{vendorId} |