Skip to content
Lopifi APIs

Xero, QuickBooks and NetSuite APIs, compared.

Three general ledgers, three different opinions about what an invoice is. Every accounting product eventually reads from somebody else’s ledger, and the gap between their model and yours is where these products fail — not in the interface, but in the assumption that the word means the same thing in all three. It does not.
Objects compared
17
Concepts
145
Divergences
53
Structural
24

How the three think

Read one row across before reading one column down. The differences are the content.

Xero

Xero Limited

A document store with few, flat nouns. One Invoice object covers sales and purchases; the general ledger is a separate, read-only, append-only feed.

Base
https://api.xero.com/api.xro/2.0/
Auth
OAuth 2.0 + Xero-Tenant-Id header
Versioning
Path version (2.0); rarely changes

Strongest

The Journals endpoint — a gapless, immutable GL feed you can replay. Nothing in QBO comes close. Since 2 March 2026 it is gated: Advanced partner tier, a security assessment and an approved use case.

Costs you

Hard product limits: two active tracking categories, one organisation per tenant, a flat chart of accounts, and a per-organisation daily call cap (5,000; 1,000 on the free Starter tier) that makes backfills a multi-day job.

81 native · 41 partial · 1 derived · 22 absent of 145 concepts compared

QuickBooks Online

Intuit

A transaction engine with a SQL-like query language. Many specific nouns, polymorphic lines discriminated by DetailType, and optimistic locking on every write.

Base
https://quickbooks.api.intuit.com/v3/company/{realmId}/
Auth
OAuth 2.0, realmId in the path
Versioning
minorversion query parameter. Since 1 August 2025 versions 1–74 are retired and every request is answered as at least 75

Strongest

Generous per-minute limits, a real query language, change data capture, a requestid for idempotent writes, and webhooks across most entities.

Costs you

No general ledger endpoint at all, no status field on transactions, no audit trail, full-replace writes that delete lines you omit — and, since the 2025 App Partner Program, read (CorePlus) calls metered per month by partner tier.

78 native · 48 partial · 3 derived · 16 absent of 145 concepts compared

NetSuite

Oracle

An ERP with its database exposed. Everything posting is a row in one transaction table with a type discriminator, wrapped in approval workflows and server-side scripts that fire on write.

Base
https://{accountId}.suitetalk.api.netsuite.com/services/rest/
Auth
OAuth 2.0 (client-credentials M2M with a certificate, or authorization code). OAuth 1.0 token-based auth still works for existing integrations, but no new TBA integrations from release 2027.1
Versioning
REST record service is v1 with a per-release schema; SOAP has a WSDL per release, the last planned one is 2025.2, no new SOAP integrations may be built from 2027.1, and SOAP is removed entirely in 2028.2

Strongest

SuiteQL — near-full SQL over the ledger — plus native subsidiaries, real accounting periods, unlimited custom segments and a field-level audit trail.

Costs you

REST list endpoints return ids only, concurrency is capped in the single digits, there are no webhooks, and a write enters a business process that can reject it for reasons not in your payload.

118 native · 18 partial · 1 derived · 8 absent of 145 concepts compared

Eight rows that change your architecture

Chosen because each answer differs and each difference is load-bearing. The object pages take every row to its conclusion.

NativeNative
— A field or object exists for it
PartialPartial
— Exists with a caveat that changes what you can build
DerivedDerived
— Not stored — you compute it
AbsentAbsent
— Nothing in the API
Headline differences between the three ledger APIs
ConceptXeroQBONetSuite
Shape of the API
Native

Few flat nouns

One Invoice object covers sales and purchases

Native

Many nouns, one query language

SQL-like SELECT over entities

Native

A database with an ERP on it

REST records, SuiteQL, SOAP, RESTlets

General ledger feedThe endpoint that decides whether you can reconcile.
Native

Journals

Read-only, gapless, replayable — Advanced partner tier only since March 2026

Absent

None

Reports API, or reconstruct from entities

Native

SuiteQL over the GL tables

Transaction status
Native

A field

DRAFT, AUTHORISED, PAID, VOIDED

Absent

None

Computed from Balance and TotalAmt

Native

A field, plus approval state

Dimensions
Partial

Two, user-named

A hard product limit

Partial

Two, vendor-named

Class and Department

Native

Unlimited custom segments

Field keys discovered at runtime

Multi-entity
Absent

One org per tenant

A group is n disconnected tenants

Absent

One company per realm

Native

Subsidiaries, elimination, consolidated FX

Audit trail
Partial

Per-record history

Coarse, not field-level

Absent

None

Timestamps, no actor

Native

Field-level system notes

Update semanticsThe difference that deletes data if you get it wrong.
Native

Merge

But a LineItems array replaces every line

Partial

Replace

Omitted lines are deleted

Native

Patch

Webhooks
Partial

Four event categories

Contacts, invoices, credit notes, subscriptions — ids only

Native

Most entities

Ids only, no data

Absent

None

Poll, or script a callout

The objects themselves

One page per object family: every field side by side, what diverges, the same fact in three payloads, and the normalised type the components consume.

Reading, writing and staying fresh

The parts that are not about accounting at all, and still decide whether the integration works.

The decisions you cannot defer

A modelling difference. Decide once, at the start — retrofitting it means a migration.

Chart of accounts

Xero has no account hierarchy

Xero

Xero's chart is a flat list: no parent field, no grouping object. The grouping in Xero's own reports comes from account type. QBO nests five levels deep in FullyQualifiedName; NetSuite nests arbitrarily.

Do — Render a flat list gracefully, with type as the only grouping. No chart-of-accounts view may assume parents exist.

Contacts, customers and suppliers

The same real party is two records in QuickBooks

QBO · NetSuite

A client who is also a subcontractor is one Contact in Xero and two unlinked records in QBO, each with its own id and balance. DisplayName is unique across customers, vendors and employees, so the two cannot even share a name — the vendor is typically 'Acme Ltd (V)'. Nothing in the API says they are the same party, so any total-exposure figure has to reunite them.

Do — Model a Party with role flags and a list of source refs. Merge is an explicit action showing both balances; auto-merge on a matching tax identifier, never on a name.

Sales invoices

QuickBooks has no invoice status

QBO

There is no Status field on a QBO Invoice: open, paid, part-paid and overdue are computed from Balance, TotalAmt and DueDate. A voided invoice keeps its record and zeroes its amounts, so counts over-report while sums do not.

Do — Derive it in one function at normalisation, store it, and mark it derived. Never present it as the source's own.

Xero serves receivables and payables from one endpoint

Xero

GET /Invoices returns ACCREC and ACCPAY documents together. An aging table fed from that endpoint without a Type filter shows payables as receivables, with plausible-looking numbers and the wrong sign on the balance sheet.

Do — Every Xero invoice read carries a Type filter. Make it a required parameter in the adapter's function signature so it cannot be forgotten.

Vendor bills

QuickBooks splits accrual from cash, and both are spend

QBO · Xero

A vendor invoice you will pay later is a Bill. A card swipe is a Purchase, which posts no accounts payable and does not appear in any Bill query. Companies that run most spend on cards can have the majority of their expenses invisible to a Bill-only integration.

Do — Every spend aggregate reads Bill and Purchase — in Xero, Invoice ACCPAY and BankTransaction SPEND. Name the metric for what it contains.

Approval is real in NetSuite and decorative elsewhere

NetSuite · Xero · QBO

Xero's SUBMITTED status is a flag any user can set. QBO's API has no approval concept whatsoever. NetSuite has server-enforced multi-step routing that your write may enter and not clear — a successful API call does not mean an approved bill.

Do — Approval state is ours for Xero and QBO, mirrored from NetSuite. Write-back is capability-gated and hidden where the capability is false.

Three-way match exists in one of the three

NetSuite

Only NetSuite models goods receipt as a record and reconciles quantity and price across PO, receipt and bill. QBO offers a link between a bill and a purchase order, with no receipt and no variance; Xero's API exposes no bill-to-PO link at all, only a purchase order whose Status has moved to BILLED.

Do — The match component computes the variance itself for QBO, from whatever quantities the linked documents carry, and labels it as our calculation rather than the ledger's. For Xero there is no link to compute from: any PO-to-bill pairing is ours, proposed and confirmed by the user.

Payments and allocation

One user action, N payment records, per-item failure

Xero

Settling five invoices with one bank receipt is one payment with five allocations on QBO and NetSuite, and five Payment records on Xero. Xero accepts them in one call — PUT /Payments takes an array, and BatchPayments settles many invoices at once — but with summarizeErrors=false each item succeeds or fails on its own. Three of five succeed and the ledger holds a half-applied receipt that reconciles to nothing.

Do — Write Xero payments through a job with a stable idempotency key, and check the result of every allocation in the response. The payment is not 'recorded' until every allocation succeeds; show a partial state honestly when it does not.

Unapplied cash is a separate object in Xero and most integrations skip it

Xero

Xero splits it into Overpayment (the customer paid too much against an invoice) and Prepayment (they paid before an invoice existed), each with its own endpoint. Neither appears in /Invoices. An AR figure built from invoice balances alone disagrees with Xero's own aging report by exactly the unapplied amount.

Do — Fetch both endpoints, model them as documents with no lines, and give unapplied cash its own row in the aging table. Tie to the ledger's aging report and surface any variance.

Journals and the general ledger

Xero's Journals endpoint is the crown jewel and is easy to miss

Xero

It emits the actual general ledger — every posting from invoices, payments, bills, bank transactions and manual journals — as immutable, sequentially numbered records. Paginate by offset on JournalNumber and you have an append-only, gapless, perfectly replayable ledger feed.

Do — Anything that reconciles or re-derives statements builds on /Journals, not on /Invoices. Store the highest JournalNumber seen as the sync cursor; it never goes backwards. Since 2 March 2026 Xero reserves the endpoint for apps on its Advanced partner tier that have passed a security assessment and had the use case approved — so make hasGlFeed a capability you discover, and keep a reconstruct-from-documents fallback for apps without it.

The Xero GL feed now sits behind a partner tier

Xero

From 2 March 2026 the Journals endpoint is available only from the Advanced tier of Xero's developer pricing, and only after an initial and annual security assessment and use-case approval. ManualJournals stays open on every tier, but it holds only the journals people typed — not the postings that invoices, payments and bank transactions generate.

Do — Treat Journals access as a per-app capability, not a property of Xero. Without it, statements are reconstructed from documents and tied to the Reports endpoints, exactly as on QBO, and the provenance label says so.

QuickBooks has no general ledger endpoint

QBO

There is no way to ask QBO for postings. Your three options are the Reports API (correct totals, presentation-shaped rows, no stable ids), reconstructing postings from every transaction entity (you then own the correctness of Intuit's tax and FX posting rules, forever), or mirroring entities via change data capture and deriving statements from the mirror.

Do — Reports API for tie-out, an entity mirror for detail. Display which one produced the number; the two will occasionally disagree.

Tax

QuickBooks US is a different product from QuickBooks elsewhere

QBO

Under automated sales tax you cannot create tax rates through the API, and the tax on a transaction is computed by Intuit from the addresses on it. Amounts you send may be silently replaced. Outside the US, GlobalTaxCalculation and explicit tax codes behave the way Xero's do.

Do — Expose canCreateTaxRates as a capability. The tax UI is read-mostly where it is false, and the write path does not promise a tax amount it cannot guarantee.

Dimensions and segments

NetSuite dimension field names are not knowable in advance

NetSuite

A custom segment creates a field whose key is defined by the customer — cseg_product, cseg_channel, cseg_whatever. Two NetSuite accounts have different dimension schemas. No type literal in your code can name them.

Do — Fetch the customsegment metadata at connection time, store it as a Dimension[], and drive every filter, group-by and column from that list.

Xero's two-category limit is a wall, not a preference

Xero

A client who wants department and project and location on Xero cannot have all three. There is no plan, no setting and no workaround inside Xero's tracking model.

Do — Say so, rather than offering a control that cannot be saved. A third dimension lives in our system, applied at the reporting layer and marked as ours.

Periods, locking and the close

Two of the three have no concept of a period

Xero · QBO

Xero and QBO have cut-off dates — two in Xero, whose effect on an API write depends on the connected user's role. There is no object that says 'August 2026 is closed', no state, no history of when it closed, and no per-module or per-entity granularity. Everything a close checklist needs to track exists nowhere in the source.

Do — Period state is owned by our system, with the ledger's lock date as one input signal. The close checklist writes to us; the lock date is a check we run against them.

Attachments and the audit trail

QuickBooks has no audit trail in the API

QBO

There is no endpoint for change history and no actor on any record. If a bookkeeper edits an invoice in QuickBooks, you will see a new LastUpdatedTime and nothing else — not what changed, not who changed it, not what it was before.

Do — Keep a before-and-after for every change we make and snapshot watched fields on every sync, so a diff can be reconstructed. Present it as our record, not the ledger's.

Reports

Report rows are presentation, not data

Xero · QBO · NetSuite

Header, section and summary rows are interleaved with data rows, nesting depth carries meaning, and identity is inconsistent. Parsing is possible; drilling from a parsed row back to the transactions behind it generally is not.

Do — Build interactive statements from ledger data. Use the report endpoint on a schedule, assert equality, and surface a variance when it appears.

Tenancy and multi-entity

Consolidation is your job on two of the three

Xero · QBO · NetSuite

A group on Xero or QBO is n disconnected tenants. You own the entity tree, the intercompany elimination and the currency translation. On NetSuite all three exist already, and ignoring them produces double-counted intercompany revenue.

Do — Own the entity tree in your schema. Map NetSuite subsidiaries into it rather than adopting them as the tree, so a mixed group — two Xero orgs plus a NetSuite subsidiary, which is common — still has a coherent parent.

Reading, paging and delta sync

NetSuite REST list endpoints return ids, not records

NetSuite

GET /record/v1/invoice returns a page of { id, links } objects. Fetching 5,000 invoices that way is 5,001 calls against a concurrency limit in the single digits. This is not an optimisation problem; it is the difference between a sync that finishes and one that does not.

Do — Use SuiteQL for every bulk read. Reserve the REST record service for single-record reads and for writes.

Deletes are invisible in two of the three

QBO · NetSuite

Xero never truly deletes — it flips a status, so a delta sync sees it. QBO surfaces deletes only through change data capture, which looks back 30 days at most, so a poll-only integration — or one that was offline for a month — accumulates ghost records forever. NetSuite hard-deletes and the record simply stops existing.

Do — Poll for changes, but reconcile the id set periodically: fetch all ids for a window and mark anything missing as gone. Without that pass, aging reports quietly include documents that no longer exist.

Writes, concurrency and rate limits

A QuickBooks update deletes the lines you omit

QBO

POST an Invoice with only the fields you meant to change and every line on it disappears. The call succeeds. The invoice is now empty and the customer's balance has moved.

Do — Read the current record, mutate it, write the whole thing back with the current SyncToken — or use sparse updates and accept their narrower semantics. A SyncToken mismatch is a real user-visible outcome, not a retryable error: someone else changed the record.

NetSuite writes can fail for reasons you cannot see

NetSuite

A user event script, a workflow condition, a mandatory custom field or a subsidiary restriction can reject a payload that is structurally perfect. The error text often names an internal field id and nothing else.

Do — Surface the raw NetSuite error to the operator, not a rewritten message. Keep the payload alongside it so support can reproduce. Never retry a rejection automatically.

Webhooks and freshness

NetSuite has no webhooks

NetSuite

The substitutes are a user event SuiteScript that calls out on save — which puts your endpoint's availability in the path of the customer's ability to save a record — or scheduled polling on lastmodifieddate.

Do — Poll by default. Offer the script only where the customer's own team maintains it, and never let a callout block a save.

The model underneath

Adapters own the difference; components never see it. If a component prop needs to know which ledger it is talking to, the canonical model is missing a field or the capability set is missing a flag.

source-ref.ts
/** Every normalised record keeps its provenance. Non-negotiable. */
type SourceRef = {
  system: "xero" | "qbo" | "netsuite";
  tenantId: string;      // Xero tenantId | QBO realmId | NetSuite accountId
  entityType: string;    // the source's own noun, verbatim
  id: string;
  raw?: unknown;         // the untouched payload, for "why is this number this number"
  fetchedAt: string;     // ISO instant
};

/** Decimal string, never a float. Currency travels with the amount. */
type Money = { amount: string; currency: string };
capabilities.ts
/** What the connected ledger can actually do. Components read this — never
 *  `source.system === "xero"`, which is wrong the moment a fourth ledger lands. */
type SourceCapabilities = {
  dimensions: Dimension[];         // 0, 1, 2, or n — discovered, not hardcoded
  lineTarget: "account" | "item" | "both";
  canCreateTaxRates: boolean;      // false for QBO US under automated sales tax
  hasApprovalWorkflow: boolean;    // NetSuite only
  hasGlFeed: boolean;              // NetSuite SuiteQL; Xero Journals only on the Advanced partner tier; false for QBO
  hasAuditTrail: boolean;          // NetSuite only
  statusIsDerived: boolean;        // true for QBO transactions
  writeSemantics: "merge" | "replace" | "patch";
  periodLock: { date?: string; perSubsidiary: boolean };
  idempotencyKey: boolean;         // Xero Idempotency-Key, QBO requestid; false for NetSuite REST
};

What is stable and what is not

Shapes are durable. Numbers are the first thing vendors change.

Every quantitative claim here — rate limits, page sizes, minor versions, the behaviour of QuickBooks’ automated sales tax — should be verified against current vendor documentation before you rely on it. Pages that lean on those numbers are marked Verify. The structural differences, which is most of this, have held for years.

The commercial terms are the newest kind of volatility. Intuit’s App Partner Program (2025) meters QuickBooks read calls per month by partner tier while leaving writes uncapped. Xero’s developer tiers (from 2 March 2026) cap connections and monthly data egress, hold the free Starter tier to 1,000 calls a day, and reserve the Journals endpoint for the Advanced tier with a security assessment. Oracle stops new SOAP and new token-based-auth integrations with NetSuite from release 2027.1 and switches SOAP off in 2028.2. None of that changes an object model — all of it changes which architecture you can afford.

When you find a difference that is not documented here, it belongs here. The failure mode this comparison exists to prevent is a per-integration workaround that nobody else learns about. Every divergence on these pages was, at some point, somebody’s surprise.

The narrative version of this comparison, with the reasoning rather than the table, is Lopifi APIs in the domain documentation.

Common questions

What does this comparison cover?

The three general ledger APIs a finance product has to integrate — Xero, QuickBooks Online and NetSuite — compared object by object across 17 objects, with 53 documented divergences in invoices, bills, the chart of accounts, journals, payments, tax, dimensions, periods and sync mechanics.

What is the biggest difference between the Xero, QuickBooks and NetSuite APIs?

Not the endpoints — the object model. The three disagree about what a line item is, where tax lives, whether a payment is its own object, and what a period close does to a posted journal. An integration written against one and ported to another usually breaks on those, not on authentication.

Why normalise them into one model?

So one component can render all three. The normalised model on these pages is what the Lopifi components consume; each divergence is named and mapped rather than papered over, because the failure mode this exists to prevent is a per-integration workaround nobody else learns about.

What are the rate limits on the Xero, QuickBooks and NetSuite APIs?

QuickBooks Online allows 500 requests a minute and 10 concurrent requests per company, and since Intuit's 2025 App Partner Program it meters read (CorePlus) calls per month — the free Builder tier is blocked above 500,000. Xero allows 60 calls a minute, 5 in flight and 5,000 a day per organisation (1,000 on its free Starter tier), with 10,000 a minute across the whole app. NetSuite limits concurrency rather than rate: 5 simultaneous requests on a Standard account, 15 on Premium, plus 10 per SuiteCloud Plus licence.

Is this the whole story?

This is the table. The narrative version, with the reasoning rather than the grid, is the Lopifi APIs document in the domain section of the documentation.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.