How does each ledger model an account? The one object all three broadly agree on, and still the one that most often breaks the profit and loss. They differ on hierarchy, on whether the account number exists at all, and on how finely the type is stated.
Concepts
11
Divergences
4
Structural
1
Endpoints
Xero
GET /Accounts
Full records, no paging on this endpoint
QBO
SELECT * FROM Account
Query endpoint, 1,000 rows per page
NetSuite
SuiteQL: FROM account
REST /record/v1/account returns ids only
The same concept in three vocabularies
Read across a row. Where a cell is absent, the concept has nowhere to live in that system — which is a design constraint, not a gap to paper over.
Native
— A field or object exists for it
Partial
— Exists with a caveat that changes what you can build
Derived
— Not stored — you compute it
Absent
— Nothing in the API
Chart of accounts fields compared across Xero, QuickBooks Online and NetSuite
Concept
Xero
QBO
NetSuite
Identifier
AccountID
GUID
Id
Numeric string, unique per entity type only
id
Numeric string
Account numberEditable everywhere, and optional in QBO. Never a stable key.
Code
String, unique-enforced, required except on bank accounts, editable
AcctNum
Absent unless the account-numbers preference is on
acctnumber
Name
Name
Name + FullyQualifiedName
Path joined with ":" — a colon in a name is a parsing hazard
acctname + displayname
Type
Type
~30 values: BANK, CURRENT, FIXED, REVENUE, DIRECTCOSTS, EXPENSE, EQUITY…
AccountType + AccountSubType
~20 types, ~80 subtypes — the finest granularity of the three
accttype
Bank, AcctRec, AcctPay, Income, COGS, Expense, OthCurrAsset, FixedAsset…
ClassThe five-way split that drives statement placement and normal balance.
Class
ASSET / LIABILITY / EQUITY / REVENUE / EXPENSE
Classification
Same five values
—
Derived from accttype
HierarchyThe reason a chart-of-accounts tree component cannot assume a parent link.
None
Flat list, no parenting at all
SubAccount + ParentRef
Up to 5 levels
parent
Arbitrary depth
Reserved / system accountsThe accounts a user must never be offered as a posting target.
An account can be restricted to a subset of subsidiaries
Disable
Status: ACTIVE / ARCHIVED
Active: true / false
isinactive
What actually differs, and what you do about it
A difference stated without a response is trivia. Each of these carries the handling.
Structural
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.
Sharp edge
Account numbers are not identifiers
Xero · QBO · NetSuite
They are editable in all three, optional on Xero bank accounts (Xero enforces Code as unique and requires it elsewhere), and in QBO absent entirely unless the company turned account numbers on. Unique is not the same as stable. A mapping table keyed on account code will silently break when a bookkeeper renumbers the chart.
Do — Key every mapping on the source id. Display the number, sort by it, let users search it — but never join on it.
Sharp edge
Type granularity does not survive a lowest-common-denominator mapping
Xero · QBO · NetSuite
All three separate receivables from other current assets at the type level — QBO AccountType 'Accounts Receivable' vs 'Other Current Asset', NetSuite AcctRec vs OthCurrAsset. QBO's AccountSubType (~80 values) gives the finest granularity of the three, below that. Xero's Type is coarser but carries SystemAccount, which QBO lacks. Collapsing all three onto one enum loses different information in each direction.
Do — Map to the coarsest common type for statement grouping, and keep the source type verbatim in a second field for tooltips, mapping UIs and support questions.
Papercut
Normal balance is not a field anywhere
Xero · QBO · NetSuite
None of the three tells you whether an account is debit-normal. All three expect you to derive it from the class — asset and expense are debit-normal; liability, equity and revenue are credit-normal — and then invert it for contra accounts: accumulated depreciation, allowance for doubtful accounts, treasury stock, sales returns and discounts. Class alone gets every one of those wrong.
Do — Derive once, at normalisation, from class plus a contra flag taken from the source subtype (QBO AccumulatedDepreciation, DiscountsRefundsGiven…) or your own mapping — never class alone — and store it. Deriving it at render time in a component is how a revenue figure ends up with the wrong sign.
The type the components consume
chart-of-accounts.ts
type Account = {
id: string;
number?: string; // display only — never a join key
name: string;
path?: string[]; // ["Operating expenses", "Travel"]; single element when flat
parentId?: string; // undefined on Xero, always
type: AccountType; // our taxonomy, coarsest common
sourceType: string; // theirs, verbatim: "DIRECTCOSTS" | "OtherCurrentAsset" | "COGS"
class: "asset" | "liability" | "equity" | "revenue" | "expense";
normalBalance: "debit" | "credit"; // class, inverted when isContra — at normalisation
isContra: boolean; // from source subtype or own mapping: accum. depreciation, allowance…
isSystem: boolean; // never offer as a posting target
currency?: string;
active: boolean;
entityIds: string[]; // NetSuite subsidiary scoping; [tenant] elsewhere
source: SourceRef;
};
What this changes in the interface
The payoff. Every difference above that reaches the surface, and what the component does about it.
Xero charts are flat; QBO and NetSuite nest
ChartOfAccounts renders a tree when depth exists and a grouped list when it does not. The indent affordance is conditional, not decorative.
Account numbers may be absent
The number column collapses rather than showing a blank gutter. Sorting falls back to name, and the sort control says so.
System accounts must not be posting targets
AccountPicker filters them out of write contexts and shows them, marked, in read contexts. The same component, two capability-driven modes.
The normalised object above is what the components read. These are the surfaces it feeds, the words for the concepts in it, and the document that explains the accounting.
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.