Skip to content

Chart of accounts

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.

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
Chart of accounts fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Identifier
Native

AccountID

GUID

Native

Id

Numeric string, unique per entity type only

Native

id

Numeric string

Account numberEditable everywhere, and optional in QBO. Never a stable key.
Native

Code

String, unique-enforced, required except on bank accounts, editable

Partial

AcctNum

Absent unless the account-numbers preference is on

Native

acctnumber

Name
Native

Name

Native

Name + FullyQualifiedName

Path joined with ":" — a colon in a name is a parsing hazard

Native

acctname + displayname

Type
Native

Type

~30 values: BANK, CURRENT, FIXED, REVENUE, DIRECTCOSTS, EXPENSE, EQUITY…

Native

AccountType + AccountSubType

~20 types, ~80 subtypes — the finest granularity of the three

Native

accttype

Bank, AcctRec, AcctPay, Income, COGS, Expense, OthCurrAsset, FixedAsset…

ClassThe five-way split that drives statement placement and normal balance.
Native

Class

ASSET / LIABILITY / EQUITY / REVENUE / EXPENSE

Native

Classification

Same five values

Derived

—

Derived from accttype

HierarchyThe reason a chart-of-accounts tree component cannot assume a parent link.
Absent

None

Flat list, no parenting at all

Partial

SubAccount + ParentRef

Up to 5 levels

Native

parent

Arbitrary depth

Reserved / system accountsThe accounts a user must never be offered as a posting target.
Native

SystemAccount

DEBTORS, CREDITORS, BANKCURRENCYGAIN, RETAINEDEARNINGS…

Derived

Implied by AccountType

"Accounts Receivable" / "Accounts Payable"

Native

specialaccounttype

Plus the AcctRec / AcctPay types

Balance on the record
Absent

—

Query the GL

Native

CurrentBalance, CurrentBalanceWithSubAccounts

Absent

—

Query the GL

Currency
Partial

CurrencyCode

Bank accounts only

Native

CurrencyRef

Native

currency

Entity scoping
Absent

—

One organisation per tenant

Absent

—

One company per realm

Native

subsidiary[]

An account can be restricted to a subset of subsidiaries

Disable
Native

Status: ACTIVE / ARCHIVED

Native

Active: true / false

Native

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.

Where the accounting is explained

What consumes this payload

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.

Compared acrossXero docsQuickBooks Online docsNetSuite docs

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.