What does an accounts-receivable invoice look like in each API? Three payloads, one journal. Xero posts a line to an account; QuickBooks posts it to an item that points at an account; NetSuite posts it to an item sublist with a segment set. And QuickBooks has no status field at all.
Concepts
11
Divergences
4
Structural
2
Endpoints
Xero
GET /Invoices?where=Type=="ACCREC"
Shared with bills — the filter is mandatory
QBO
SELECT * FROM Invoice
Its own entity
NetSuite
SuiteQL: FROM transaction WHERE type='CustInvc'
REST /record/v1/invoice for single records
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
Sales invoices fields compared across Xero, QuickBooks Online and NetSuite
Concept
Xero
QBO
NetSuite
Object identityWhether the noun exists on its own, or is a mode of a shared one.
Invoice with Type: ACCREC
Same object and endpoint as bills
Invoice
Its own entity
invoice
type discriminator on the transaction table
Document number
InvoiceNumber
Unique, auto-assigned if omitted
DocNumber
Uniqueness is a company preference
tranid
StatusThe single most consequential row on this page.
A difference stated without a response is trivia. Each of these carries the handling.
Structural
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.
Structural
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.
Sharp edge
QuickBooks wants an item where Xero wants an account
QBO
You cannot post a QBO invoice line directly to an income account — the line references an Item, and the item carries the account. Creating invoices from a source that only knows accounts means resolving or creating a service item per account, and that item then lives in the customer's product list permanently.
Do — Maintain a small, explicitly named set of integration items rather than creating one per account on the fly. Show the user which item a line will use before the first write.
Sharp edge
Dimensions sit at different levels
Xero · QBO · NetSuite
Xero always puts tracking on the line. QBO puts Department on the header and Class on either, depending on a company preference. NetSuite allows both and lets the line override the header.
Do — Fan header values down to every line at normalisation and record that you did, so an edit UI does not write per-line values back to a system that cannot store them.
The same fact, different payloads
Stacked rather than tabbed — a reader comparing two shapes should not have to hold one of them in memory.
type LedgerDocument = {
id: string;
type: "sales_invoice" | "purchase_bill" | "sales_credit" | "purchase_credit"
| "cash_sale" | "cash_purchase" | "journal";
number?: string;
party?: Party;
issueDate: string; // calendar date, no time, no zone
dueDate?: string;
currency: string;
exchangeRate?: string;
total: Money;
totalBase?: Money; // from the source — never recomputed as amount × rate
outstanding: Money;
/** Ours. Derived where the source has none. */
status: "draft" | "awaiting_approval" | "open" | "part_paid" | "paid" | "voided" | "voided_or_zero" | "deleted";
statusIsDerived: boolean;
sourceStatus?: string; // theirs, for the tooltip
lines: DocumentLine[];
allocations: Allocation[];
periodId?: string;
source: SourceRef;
};
/** QBO status is computed, never read. One function, one place. */
function qboInvoiceStatus(inv: QboInvoice, today: string): DocumentStatus {
if (inv.PrivateNote?.startsWith("Voided")) return "voided"; // voided invoices keep TotalAmt 0
if (inv.TotalAmt === 0 && inv.Balance === 0) return "voided_or_zero"; // a legitimate $0 invoice looks the same
if (inv.Balance === 0) return "paid";
if (Number(inv.Balance) < Number(inv.TotalAmt)) return "part_paid";
if (inv.DueDate && inv.DueDate < today) return "open"; // overdue is a view, not a status
return "open";
}
What this changes in the interface
The payoff. Every difference above that reaches the surface, and what the component does about it.
QBO status is derived
The status pill on a QBO document is annotated as derived, and the detail drawer shows the balance arithmetic behind it. A user reconciling to QuickBooks must be able to see why we said 'part paid'.
Xero shares an endpoint between AR and AP
Every ledger table takes an explicit document type. There is no 'all invoices' view, because there is no such thing.
Item is required on some sources and absent on others
The line editor's item field is required per capability, never per component. The same InvoiceForm renders an account picker on Xero and an item picker on QBO.
Overdue is a computed view, not a stored state
Aging buckets are computed against an explicit as-of date shown in the header, not against 'now'. See the AsOf component.
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.