Skip to content

Tenancy and multi-entity

What is the boundary of one set of books? A Xero tenant and a QuickBooks realm are each one company, full stop. A NetSuite account holds a subsidiary tree with elimination and consolidated rates built in. A group spanning both is the normal case, and only one of them will help you.

Concepts
8
Divergences
3
Structural
1

Endpoints

Xero

GET /connections, GET /Organisation

One organisation per tenant id

QBO

GET /companyinfo/{realmId}

One company per realm

NetSuite

SuiteQL: FROM subsidiary

A hierarchy inside one account

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
Tenancy and multi-entity fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Tenant unit
Native

Organisation

Identified by tenantId, a GUID, in a header

Native

Company

Identified by realmId, in the path

Native

Account

Containing many subsidiaries

Record id uniquenessThe first thing that breaks a global search.
Native

GUID, globally unique

Partial

Unique per entity type only

Invoice "145" and Bill "145" both exist

Partial

Unique per record type

Multiple entities
Absent

Multiple tenants

Disconnected; you consolidate

Absent

Multiple realms

Disconnected; you consolidate

Native

Subsidiary hierarchy

OneWorld

Intercompany elimination
Absent

—

Absent

—

Native

Native, with elimination subsidiaries

Consolidated FX
Absent

—

Each org has its own base currency

Absent

—

Native

Consolidated exchange rates per period

Entity on a record
Derived

Implicit in the tenant

Derived

Implicit in the realm

Native

subsidiary field

Required on write, filtered on read

Base currency
Native

Organisation.BaseCurrency

Native

CompanyInfo home currency

Native

Per subsidiary, plus consolidation

Token scope
Native

One token, many tenants

Switch with the header

Partial

One token per realm

Partial

One credential per account

What actually differs, and what you do about it

A difference stated without a response is trivia. Each of these carries the handling.

Sharp edge

QuickBooks ids are not globally unique

QBO · NetSuite

Ids are unique per entity type within a realm. Invoice 145 and Bill 145 are different documents. Any cache key, URL or React key built from the id alone will collide, and the collision looks like data corruption rather than a key bug.

Do — Key everything as {system}:{tenantId}:{entityType}:{id}. Make the canonical id a branded string so a bare source id cannot be assigned to it.

Structural

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.

Sharp edge

Xero switches tenant with a header, and it is easy to get wrong

Xero

The same access token serves every connected organisation; the Xero-Tenant-Id header decides which one you are talking to. A misthreaded header writes an invoice into the wrong company's books, and the API cannot tell you that you meant otherwise.

Do — Never carry the tenant id in ambient context. Pass it explicitly through every adapter call, and assert it against the record's expected tenant before any write.

The type the components consume

tenancy.ts
/** Branded, so a bare source id cannot be passed where a canonical one belongs. */
type LedgerId = string & { readonly __ledgerId: unique symbol };

const ledgerId = (s: SourceRef): LedgerId =>
  `${s.system}:${s.tenantId}:${s.entityType}:${s.id}` as LedgerId;

type Entity = {
  id: string;
  name: string;
  parentId?: string;           // our tree, not the source's
  baseCurrency: string;
  /** How this entity's books are reached. One entity, one connection. */
  connection: { system: SystemId; tenantId: string; subsidiaryId?: string };
  eliminates?: boolean;        // NetSuite elimination subsidiary
};

What this changes in the interface

The payoff. Every difference above that reaches the surface, and what the component does about it.

An entity may be one of several in a group
EntityMark appears wherever a figure belongs to one entity, and consolidated figures state their basis and their elimination treatment.
Ids collide across entity types
Every deep link and cache key carries system, tenant and entity type. Search results show the source and entity alongside the number.

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.