QuickBooks Online is Intuit's cloud accounting product and the default small-business ledger across the United States, Canada, the United Kingdom and Australia. Its Accounting API is a mature REST API with one base URL, one path shape (/v3/company/<realmId>/<entity>), an SQL-like query language instead of per-entity filters, and a real sandbox. For a commerce database it gives you the sales document (Invoice or SalesReceipt), the inventory item with a live QtyOnHand, the customer, and the payment, and it lets you write all of them back. Two things distinguish it from the other accounting APIs in this set: the query language, which is a pared-down SELECT rather than query-string filters, and change data capture, a single endpoint that tells you everything that moved across many entity types since a timestamp.
At a glance
What it is
Intuit Inc. is a Mountain View company whose accounting products dominate US small business. QuickBooks Online is the cloud product; QuickBooks Desktop is the older installed product, still widely used, and reached through an entirely different mechanism described at the end of the write-back section. QuickBooks Online is sold in the US, Canada, the UK, Australia and a global edition, and localises sales tax, payroll and reporting per region.
The domain model is accounting-first. A company file is a realmId. Within it, sales are recorded as either an Invoice (customer pays later, creates a receivable) or a SalesReceipt (customer paid at the point of sale, no receivable). Refunds are a CreditMemo (credit against the customer's balance) or a RefundReceipt (money actually returned). Products are Item records, and only items with Type of Inventory and TrackQtyOnHand true carry a quantity. Locations in the warehouse sense do not exist: QuickBooks Online has no multi-warehouse inventory, which is the single largest gap for a commerce use case.
There is no native sales order entity. Intuit documents a sales order workflow built on Estimate and Invoice, so an order that is not yet fulfilled is usually modelled as an Estimate, converted to an Invoice on fulfilment, or as an Invoice created directly.
API access
- Create a developer account at developer.intuit.com and create an app from the dashboard. You get two sets of credentials: Development keys for sandbox, Production keys for live companies.
- Add at least one redirect URI per environment.
- Create a sandbox company from the dashboard to develop against. Sandbox uses the same OAuth server but a different API base URL.
- To connect real companies beyond your own, and to list on the QuickBooks App Store, the app goes through Intuit's review process (technical, security and marketing requirements are published separately).
Base URLs:
URI shapes, quoted from Intuit's REST features page:
Minor versions. The v3 major version is stable; behaviour changes arrive as numbered minor versions appended as a query parameter, ?minorversion=75. Minor version 75 is the latest listed, released 16 December 2024, adding DefaultTimeZone to CompanyInfo. Pin a minor version explicitly rather than taking the default, because the default moves.
Official SDKs, all under Intuit's own GitHub organisations: .NET, PHP, Java, plus OAuth-only clients for Node.js, Python and Ruby. Intuit also publishes an official MCP server and a Postman collection linked from the sandbox docs.
Intuit's documentation pages are rendered client-side and return an empty shell to non-browser clients. Everything quoted on this page comes from those official URLs read through a rendering proxy, not from third-party mirrors. The discovery documents and SDK sources were fetched directly and agree.
Authentication
OAuth 2.0 authorization code. Intuit publishes OpenID Connect discovery documents, and taking the endpoints from there rather than hard-coding them is the documented practice.
- Production discovery:
https://developer.api.intuit.com/.well-known/openid_configuration - Sandbox discovery:
https://developer.api.intuit.com/.well-known/openid_sandbox_configuration
Both currently resolve to the same core endpoints:
{
"issuer": "https://oauth.platform.intuit.com/op/v1",
"authorization_endpoint": "https://appcenter.intuit.com/connect/oauth2",
"token_endpoint": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
"userinfo_endpoint": "https://accounts.platform.intuit.com/v1/openid_connect/userinfo",
"revocation_endpoint": "https://developer.api.intuit.com/v2/oauth2/tokens/revoke",
"jwks_uri": "https://oauth.platform.intuit.com/op/v1/jwks",
"claims_supported": ["aud", "exp", "iat", "iss", "realmid", "sub"]
}
- Redirect the user to the authorization endpoint with the accounting scope.
GET https://appcenter.intuit.com/connect/oauth2 ?client_id=Q3ylJatCvnkYqVKLmkxxxxxxxxxxxxxxxkYB36b5mws7HkKUEv9aI &response_type=code &scope=com.intuit.quickbooks.accounting &redirect_uri=https://www.mydemoapp.com/oauth-redirect &state=security_token%3D138r5719ru3e1
- Read the redirect. Three parameters come back, and the third is the one that matters:
https://www.mydemoapp.com/oauth-redirect ?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7 &state=security_token%3D138r5719ru3e1 &realmId=1231434565226279
realmId is the QuickBooks Online company id, also called the company ID. It is not in the token. It goes in the URL of every subsequent API call, and it must be stored alongside the tokens.
- Exchange the code. Client credentials go in an HTTP basic header,
"Basic " + base64encode(client_id + ":" + client_secret). Send exactly one exchange request: Intuit warns that repeating it may invalidate the tokens.
curl -X POST 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer' \ -H 'Accept: application/json' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Authorization: Basic <base64(client_id:client_secret)>' \ -H 'x-include-refresh-token-hard-expires-in: true' \ -d 'grant_type=authorization_code' \ -d 'code=L3114709614564VSU8JSEiPkXx1xhV8D9mv4xbv6sZJycibMUI' \ -d 'redirect_uri=https://www.mydemoapp.com/oauth-redirect'
Response:
{
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "Q311488394272qbajGfLBwGmVsbF6VoNpUKaIO5oL49aXLVJUB",
"x_refresh_token_expires_in": 8640000,
"x_refresh_token_hard_expires_in": 157680000,
"access_token": "eJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGGlyIn0..."
}
Read those numbers carefully, because they drive the whole credential store design:
expires_inis 3600: the access token lives one hour.x_refresh_token_expires_inis 8,640,000 seconds, which is 100 days. The refresh token expires if unused, and Intuit returns a new refresh token periodically, so a connector must persist whatever refresh token comes back on every refresh rather than assuming it is stable.x_refresh_token_hard_expires_inis 157,680,000 seconds, five years: the absolute ceiling on the grant regardless of refresh activity.
Refresh with
grant_type=refresh_tokenagainst the same token endpoint and the same basic auth header. If the refresh token expires, the user has to reauthorise from step 1.Call the API. The header is a standard bearer, and the company is in the path.
curl 'https://quickbooks.api.intuit.com/v3/company/1231434565226279/invoice/130?minorversion=75' \ -H 'Authorization: Bearer eyJlbmMiOiJ...' \ -H 'Accept: application/json'
Scopes. com.intuit.quickbooks.accounting is the one that matters here. com.intuit.quickbooks.payment covers the Payments API, and the OpenID scopes openid, profile, email, phone, address give you the user identity through the userinfo endpoint. Changing an app's scopes forces every connected user through the consent flow again.
Multi-account. One app, one client id, and per connected company you store a realmId, an access token and a refresh token. Unlike some platforms, a single user authorising twice for two companies produces two realmId values and two token sets. Key your credential store on realmId.
Objects we can read
Reads come in three shapes: read by id, query, and change data capture.
Read by id: GET /v3/company/<realmId>/<entity>/<id>.
Query: GET /v3/company/<realmId>/query?query=<select>, or POST to the same path with Content-Type: application/text and the statement as the body, which is the better choice once statements get long.
Select Statement = SELECT * | count(*) FROM IntuitEntity [WHERE WhereClause] [ORDERBY OrderByClause] [STARTPOSITION Number] [MAXRESULTS Number]
The limitations are published and are sharp. No projections, so you always get all populated attributes. No OR in WHERE. No GROUP BY, no JOIN. One entity per statement. LIKE supports only %. On the Id field only = and IN work, not the comparison operators. Null is written as a space inside single quotes.
Pagination is STARTPOSITION and MAXRESULTS, both 1-based. The maximum returned in one response is 1,000 entities; the default when MAXRESULTS is omitted is 100. count(*) gives you the total up front. The response carries startPosition and maxResults on QueryResponse, and sparse: true if not all attributes were returned.
Change data capture is the incremental sync primitive and the reason a QuickBooks connector can be efficient:
GET https://quickbooks.api.intuit.com/v3/company/<realmId>/cdc ?entities=Invoice,SalesReceipt,Item,Customer,Payment,CreditMemo &changedSince=2026-09-21T10:00:00-07:00
It returns the full body of every entity of those types that changed since the timestamp, and a status of Deleted for entities that were removed. The documented constraints: it looks back at most 30 days, responses hold at most 1,000 objects so short windows are recommended, and it works for all entities except JournalCode, TaxAgency, TimeActivity, TaxCode and TaxRate.
{
"CDCResponse": [{
"QueryResponse": [{
"Customer": [{ "Id": "63" }, { "Id": "99" }],
"startPosition": 1,
"maxResults": 2
}, {
"Estimate": [
{ "Id": "34" },
{ "Id": "123" },
{
"domain": "QBO",
"status": "Deleted",
"Id": "979",
"MetaData": { "LastUpdatedTime": "2015-12-23T12:55:50-08:00" }
}
],
"startPosition": 1,
"maxResults": 3,
"totalCount": 5
}]
}]
}
Orders
There is no SalesOrder entity. Use Invoice for a billed sale and SalesReceipt for a paid-at-point-of-sale one, with Estimate standing in for an unfulfilled order when the seller's workflow needs one. Intuit documents a "Manage sales orders" workflow built on exactly that combination.
GET /v3/company/<realmId>/query?query= SELECT * FROM Invoice WHERE MetaData.LastUpdatedTime > '2026-09-01T00:00:00-07:00' ORDERBY MetaData.LastUpdatedTime STARTPOSITION 1 MAXRESULTS 500
Read by id:
{
"Invoice": {
"domain": "QBO",
"Id": "130",
"SyncToken": "0",
"DocNumber": "1037",
"TxnDate": "2014-09-19",
"DueDate": "2014-10-19",
"CustomerRef": { "name": "Sonnenschein Family Store", "value": "24" },
"SalesTermRef": { "value": "3" },
"TotalAmt": 362.07,
"Balance": 362.07,
"Deposit": 0,
"PrintStatus": "NeedToPrint",
"EmailStatus": "NotSet",
"ApplyTaxAfterDiscount": false,
"sparse": false,
"MetaData": {
"CreateTime": "2014-09-19T13:16:17-07:00",
"LastUpdatedTime": "2014-09-19T13:16:17-07:00"
},
"CustomerMemo": { "value": "Thank you for your business and have a great day!" },
"TxnTaxDetail": {
"TxnTaxCodeRef": { "value": "2" },
"TotalTax": 26.82,
"TaxLine": [{
"DetailType": "TaxLineDetail",
"Amount": 26.82,
"TaxLineDetail": {
"NetAmountTaxable": 335.25,
"TaxPercent": 8,
"TaxRateRef": { "value": "3" },
"PercentBased": true
}
}]
},
"LinkedTxn": [{ "TxnId": "100", "TxnType": "Estimate" }],
"BillEmail": { "Address": "Familiystore@intuit.com" },
"ShipAddr": {
"Id": "25",
"Line1": "5647 Cypress Hill Ave.",
"City": "Middlefield",
"CountrySubDivisionCode": "CA",
"PostalCode": "94303",
"Lat": "37.4238562",
"Long": "-122.1141681"
},
"CustomField": [
{ "DefinitionId": "1", "StringValue": "102", "Type": "StringType", "Name": "Crew #" }
]
},
"time": "2026-07-24T10:48:27.082-07:00"
}
ShipAddr and BillEmail are PII. LinkedTxn is the graph edge: it is how an Invoice points at the Estimate it came from, how a Payment points at the Invoice it settles, and how a CreditMemo points at what it credits.
Shipping fields appear when populated: ShipDate, TrackingNum, ShipMethodRef, and a shipping charge arrives as an ordinary SalesItemLineDetail line referencing the shipping item.
Order items
Lines are a polymorphic array discriminated by DetailType. The types you will meet on a sale: SalesItemLineDetail (a real product or service line), SubTotalLineDetail (an automatically inserted running subtotal, with no item), DiscountLineDetail (a percentage or amount discount), GroupLine (a bundle) and DescriptionOnlyLine.
"Line": [
{
"Id": "1",
"LineNum": 1,
"Description": "Rock Fountain",
"Amount": 275.0,
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": {
"ItemRef": { "name": "Rock Fountain", "value": "5" },
"Qty": 1,
"UnitPrice": 275,
"TaxCodeRef": { "value": "TAX" }
}
},
{
"DetailType": "SubTotalLineDetail",
"Amount": 335.25,
"SubTotalLineDetail": {}
},
{
"DetailType": "DiscountLineDetail",
"Amount": 5.0,
"DiscountLineDetail": {
"PercentBased": true,
"DiscountPercent": 10,
"DiscountAccountRef": { "name": "Discounts given", "value": "30" }
}
}
]
Filter out SubTotalLineDetail lines before summing, or you will double-count. Tax is not on the line: it is in TxnTaxDetail at document level, with per-rate TaxLine entries. A maximum of 10,000 line items per transaction is documented.
Products and listings
GET /v3/company/<realmId>/item/<itemId>, or query SELECT * FROM Item.
{
"Item": {
"Id": "37",
"Name": "Office Supplies",
"FullyQualifiedName": "Office Supplies",
"Description": "This is the sales description.",
"PurchaseDesc": "This is the purchasing description.",
"Active": true,
"Taxable": true,
"Type": "Inventory",
"TrackQtyOnHand": true,
"QtyOnHand": 10,
"InvStartDate": "2013-02-19",
"UnitPrice": 25,
"PurchaseCost": 35,
"IncomeAccountRef": { "name": "Sales of Product Income", "value": "79" },
"AssetAccountRef": { "name": "Inventory Asset", "value": "81" },
"ExpenseAccountRef": { "name": "Cost of Goods Sold", "value": "80" },
"SyncToken": "0",
"sparse": false,
"domain": "QBO",
"MetaData": {
"CreateTime": "2015-04-22T11:03:23-07:00",
"LastUpdatedTime": "2015-04-22T11:03:24-07:00"
}
},
"time": "2015-04-22T11:01:37.346-07:00"
}
Type is Inventory, NonInventory, Service, Group (bundle) or Category. Only Inventory items with TrackQtyOnHand true carry QtyOnHand. Sku exists as an optional field on the Item and is frequently empty, because QuickBooks users often use Name as the SKU. Name is unique within a company and is the practical key, with FullyQualifiedName giving the category path.
There is no listing object: no channel id, no URL, no channel status. Active is the only published or unpublished flag.
Inventory
QtyOnHand on the Item is the entire inventory model. There is no location dimension, no reserved quantity, no inbound quantity, and no separate inventory resource. Locations in the QuickBooks sense (Department, sometimes labelled Location in the UI) are a reporting dimension on transactions, not a stock-holding place.
InvStartDate is the date from which quantity tracking began, which matters when reconciling historical movements.
Shipments and tracking
No shipment entity. An Invoice or SalesReceipt can carry ShipDate, TrackingNum and ShipMethodRef, which is a free-text or reference shipping method rather than a carrier code. There are no tracking events and no delivered timestamp. Carrier state must come from a courier connector and be joined on DocNumber or a custom field.
Returns and cancellations
Two entities, and the difference is whether money left the bank.
- CreditMemo: a credit against the customer's balance, which can then be applied to an invoice. Read by id or query; apply it with a
Paymentor by linking. - RefundReceipt: money actually returned to the customer.
Cancellation is a void, not a delete: POST /v3/company/<realmId>/invoice?operation=void with the Id and SyncToken in the body. Voided invoices remain readable with zeroed amounts. Deletion also exists (?operation=delete) and genuinely removes the record, so a connector should never delete.
Payments and settlements
Payment records money received against one or more invoices. SalesReceipt records a sale already paid, with a PaymentMethodRef and a DepositToAccountRef. Deposit records money arriving in a bank account, which is the closest thing to a settlement when a marketplace pays out in a lump.
The link from payment to invoice is in Payment.Line[].LinkedTxn[], which is the join to compute what an invoice was actually paid by.
There is no marketplace settlement object. Intuit's separate Payments API (com.intuit.quickbooks.payment scope) covers card processing and is a different product.
Customers
GET /v3/company/<realmId>/customer/<id> or SELECT * FROM Customer. Fields include DisplayName (unique within the company), GivenName, FamilyName, CompanyName, PrimaryEmailAddr.Address, PrimaryPhone.FreeFormNumber, Mobile, BillAddr, ShipAddr, Balance, Taxable, Active, Job and ParentRef for sub-customers, which is how QuickBooks models jobs and projects under a customer.
All of that is unmasked PII. Addresses carry geocoded Lat and Long, which come back as the literal string INVALID when geocoding failed, so do not parse them as numbers without a guard.
Locations
Not a stock location. Department is a reporting dimension (branch, store, division) enabled per company, and Class is a second, orthogonal dimension. Both appear on transactions as DepartmentRef and ClassRef. If a seller uses Department to mean "shop", that is a convention, not a model.
Writing back: sales orders, invoices and stock adjustments
Writes are POSTs to the entity path. Four operation styles:
- Create:
POST /v3/company/<realmId>/<entity>with noIdin the body. - Full update:
POSTto the same path withIdand the currentSyncToken, and the complete body. Anything omitted is cleared. - Sparse update: the same, plus
"sparse": true. Only the attributes present are changed. - Void or delete:
POST /v3/company/<realmId>/<entity>?operation=voidor?operation=deletewithIdandSyncToken.
SyncToken is mandatory on every update and is optimistic concurrency: it must match the server's current value or the write is rejected. Read, then write, and do not cache a SyncToken across a long interval.
Invoices
POST /v3/company/<realmId>/invoice. Documented business rules: at least one line that is a sales item or an inline subtotal, and a populated CustomerRef. DocNumber is auto-assigned if omitted. If ShipAddr or BillAddr are omitted, the customer's addresses are used.
Minimum request body:
{
"Line": [
{
"DetailType": "SalesItemLineDetail",
"Amount": 100.0,
"SalesItemLineDetail": {
"ItemRef": { "name": "Services", "value": "1" }
}
}
],
"CustomerRef": { "value": "1" }
}
The response is the complete created invoice, including the generated Id, DocNumber, SyncToken, the inserted SubTotalLineDetail line, computed TotalAmt and Balance, and MetaData.
Emailing it is a separate call: POST /v3/company/<realmId>/invoice/<invoiceId>/send, or with ?sendTo=<emailAddr> to override the address. That sets EmailStatus to EmailSent and populates DeliveryInfo. A PDF is GET /v3/company/<realmId>/invoice/<invoiceId>/pdf with Accept: application/pdf.
For a paid-at-checkout marketplace order, SalesReceipt is usually the better fit than Invoice plus Payment, because it is one document and leaves no receivable open.
Sales orders
Not a distinct entity. Two workable patterns:
- Estimate then Invoice. Create an
Estimatewhen the order is placed, then create anInvoiceon fulfilment and link it withLinkedTxnofTxnTypeEstimate. This mirrors what Intuit's sales order workflow documentation describes and keeps unfulfilled demand visible. - Invoice directly. Simpler, but the ledger then carries revenue for goods that have not shipped, which an accountant will object to.
Stock adjustments
There is no inventory adjustment entity in the published entity list. Stock is changed in one of two ways:
- Set
QtyOnHandon the Item with a full or sparse update. This is an absolute set, not a delta, which is the opposite of most accounting systems and is convenient for a marketplace connector. It needs the currentSyncToken, so read then write. - Post a transaction that moves stock. An Invoice or SalesReceipt line referencing an inventory item decrements it; a Bill, Purchase or Purchase Order receipt increments it. This is the accounting-correct route because it leaves an audit trail and posts to Cost of Goods Sold.
Setting QtyOnHand directly writes an inventory quantity adjustment behind the scenes and will affect the company's books. It also races: because it requires a SyncToken read first, two concurrent writers will have one rejected. Serialise stock writes per item, retry on SyncToken mismatch by re-reading, and agree with the seller's bookkeeper before using it at all.
Batch and idempotency
POST /v3/company/<realmId>/batch takes up to a recommended 30 operations in one request, each with a bId you choose so responses can be matched back. Throttled separately at 40 batch requests per minute per realm and app.
There is no idempotency key. The available guards: set DocNumber yourself and query for it before creating, or put an external order id in a CustomField and query on that. DocNumber uniqueness can be enforced per company in QuickBooks settings, which turns a duplicate create into an error instead of a duplicate.
QuickBooks Desktop
QuickBooks Desktop, including Enterprise, Premier, Pro and Point of Sale, is not reachable by this API at all. The bridge is the QuickBooks Web Connector, a Windows application installed on the same machine or LAN as QuickBooks. Intuit describes it as a go-between that passes qbXML and qbposXML between a web application and QuickBooks Desktop. The integration is inverted relative to a normal API: your web service implements a SOAP interface defined by the Web Connector's WSDL, the user installs a .qwc XML file describing your service, and the Web Connector, running beside QuickBooks, initiates every exchange on a schedule or on demand. Because the Web Connector calls out, no firewall ports need opening.
Version support matters: Web Connector 2.1.0.30 and older work with QuickBooks Desktop products back to 2002 and with Point of Sale v4.0 or later, but support only TLS 1.0. Version 2.2.0.34 and newer support TLS 1.0 through 1.2 but only work with QuickBooks 2015 and later, and drop Point of Sale. A connector targeting Desktop is therefore a different product from a QuickBooks Online connector: different protocol (SOAP and qbXML), different data model, different deployment (an agent on the customer's machine), and a polling cadence controlled by the customer's schedule rather than yours.
Webhooks and notifications
Webhooks are configured per app, not per company. You set one production endpoint and one development endpoint in the developer dashboard, select which entity and operation combinations to receive, and then get notifications for every QuickBooks Online company connected to your app.
Supported entities and operations, as published:
Verification. After configuring the endpoint, Intuit issues an app-specific verifier token. To verify a notification: HMAC-SHA256 the raw notification body using the verifier token as the key, base64-encode it, and compare to the intuit-signature header. A sample header set:
content-type: application/json; charset=utf-8 intuit-created-time: 2016-02-02T16:25:00-0800 intuit-t-id: 9cf50b60-8b0e-4fea-8327-e6a66099fe6f intuit-notification-schema-version: 0.1 intuit-signature: 6kQBQtjwjupelRMwkyJsnpq80uhz2o+Rn92+m03GhKE= user-agent: intuit_notification_server/0.1
Body shape. Intuit changed the notification body in late 2025 to a CloudEvents array. Each element:
[
{
"specversion": "1.0",
"id": "88cd52aa-33b6-4351-9aa4-47572edbd068",
"source": "intuit.dsnBgbseACLLRZNxo2dfc4evmEJdxde58xeeYcZliOU=",
"type": "qbo.account.created.v1",
"datacontenttype": "application/json",
"time": "2025-09-10T21:31:25.179851517Z",
"intuitentityid": "1234",
"intuitaccountid": "310687",
"data": {}
}
]
type is namespace.entitytype.eventname.version, for example qbo.salesreceipt.updated.v1. intuitaccountid is the realmId, so one delivery can mix events from several companies and a handler must route per element. intuitentityid is the changed entity's id. data is usually empty; it carries extras for specific cases, such as deletedid on qbo.customer.merged.v1 and an alternative_ids array on qbo.employee.created.v1. The notification never carries the entity body, so every event costs a follow-up read.
Delivery contract. Respond HTTP 200 within 3 seconds. Failures are retried at 10s, 20s, 30s, 5m, 20m, 2h, 4h, 6h and then every 6 hours, and you may not receive later events until the first is acknowledged. Process asynchronously off a queue. Events can arrive out of order; the time field is the source of truth. There is no Intuit-imposed cap on body size or event count, so your own server limit (2 MB is a common default) is the practical one.
Intuit's own recommendation is to use webhooks and CDC together: on top of webhooks, make a change data capture call back to the last successfully processed event for each entity, plus a daily CDC sweep. Treat webhooks as a latency optimisation over a CDC poll, never as the only source.
Rate limits and pagination
Published limits, as of September 2026:
Separately, apps in the Builder tier of the Intuit App Partner Program have an included allowance of 500,000 CorePlus API calls per workspace per month, above which calls are throttled with HTTP 429.
Throttling returns HTTP 429 and Intuit's instruction is explicit: wait 60 seconds before retrying. No quota headers are documented, so a client cannot see remaining budget and must count locally and back off on 429.
A cadence that fits comfortably: webhooks for latency, a CDC call every 5 to 15 minutes per company over the entity list you care about, and a daily CDC sweep over a 24-hour window as the safety net. At one CDC call per company per 5 minutes that is 288 calls per company per day, far inside 500 per minute, and the real constraint becomes the follow-up reads triggered by webhook events.
Mapping to the unified model
Gaps and open questions
- The developer portal is not machine-readable. Every documentation page is client-rendered and returns an empty shell to a plain HTTP client. The content here was read through a rendering proxy against the official URLs, and the discovery documents and SDK sources were fetched directly and agree with it. Intuit publishes no OpenAPI document for the Accounting API, so field-level truth has to come from the API Explorer or the SDK entity classes.
- No sales order entity. The mapping from a marketplace order to QuickBooks is a modelling decision, not a lookup. Estimate, Invoice and SalesReceipt each have accounting consequences, and the right choice depends on the seller's bookkeeper. Confirm before building.
- No multi-location inventory. This is a hard limit of the product, not of the API. Sellers with more than one warehouse either do not track stock in QuickBooks or use a third-party inventory app that syncs a single aggregate figure back.
- Setting
QtyOnHandhas accounting effects. The exact journal QuickBooks posts when you set it through the API, and which adjustment account it uses, was not established here. Verify in a sandbox before writing stock in production. - Webhook entity coverage. The published table names the entities and operations, but for several entities the exact operation set was rendered as a grid that did not translate cleanly. Verify the specific entity and operation you depend on in the developer dashboard, where the selectable list is authoritative.
- Refresh token rotation. Intuit returns a new refresh token periodically and the old one stops working. The exact rotation interval was not published on the pages read. Always persist the refresh token returned by every refresh.
- App Partner Program economics. The 500,000 CorePlus calls per workspace per month allowance applies to the Builder tier; what "CorePlus" counts and what the overage costs was not established here and should be checked with Intuit before designing a high-volume sync.
Sources
All Intuit URLs below were read through a rendering proxy because the pages are client-rendered; the discovery documents and GitHub sources were fetched directly.
- Intuit OpenID discovery document, production and sandbox: authorization, token, userinfo, revocation and JWKS endpoints, and the
realmidclaim - OAuth 2.0 for QuickBooks Online: authorization request, the
realmIdon the redirect, the basic-auth token exchange, and the token response withexpires_in,x_refresh_token_expires_inandx_refresh_token_hard_expires_in - Invoice entity reference: business rules, create, read, query, send, void, delete, sparse and full update, and the sample bodies quoted above
- Item entity reference: the item object including
TrackQtyOnHand,QtyOnHand,InvStartDateand the account references - Query operations and syntax: the select statement grammar, its limitations,
STARTPOSITIONandMAXRESULTS, and the 1,000 and 100 response sizes - Change data capture operation: the
cdcendpoint, the 30-day look-back, the 1,000-object response cap, the excluded entities and thestatus: Deletedmarker - API call limits and throttles: every number in the rate limit table
- Basic schema and data formats: the URI shapes and required headers
- Minor versions of our API: minor version 75, 16 December 2024
- Webhooks, Configure webhooks, Understanding the data object and Best practices: the entity and operation table, the verifier token and
intuit-signatureHMAC-SHA256 scheme, the CloudEvents body, the 3-second acknowledgement rule and the retry schedule - Get started with QuickBooks Web Connector: the qbXML and SOAP bridge to QuickBooks Desktop, the
.qwcfile, and the version and TLS support matrix - intuit/QuickBooks-V3-PHP-SDK, source read:
src/Core/HttpClients/SyncRestHandler.phpconfirming the?minorversion=query parameter - intuit/quickbooks-online-mcp-server: Intuit's own MCP server, whose tool list corroborates the entity coverage