Skip to content

Sales invoices

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.

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
Sales invoices fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Object identityWhether the noun exists on its own, or is a mode of a shared one.
Partial

Invoice with Type: ACCREC

Same object and endpoint as bills

Native

Invoice

Its own entity

Native

invoice

type discriminator on the transaction table

Document number
Native

InvoiceNumber

Unique, auto-assigned if omitted

Partial

DocNumber

Uniqueness is a company preference

Native

tranid

StatusThe single most consequential row on this page.
Native

Status

DRAFT, SUBMITTED, AUTHORISED, PAID, VOIDED, DELETED

Absent

None

Derive from Balance, TotalAmt and DueDate. A void zeroes the amounts

Native

status + approvalstatus

Open, Paid In Full, Pending Approval…

Outstanding amount
Native

AmountDue

Native

Balance

Native

amountremaining

Line targetWhere the revenue actually lands.
Native

AccountCode, or ItemCode

Account-based lines are the norm

Partial

ItemRef

Effectively required; the item carries IncomeAccountRef

Partial

item sublist

Item required; the item carries the income account

Line polymorphism
Native

Uniform LineItems[]

Native

DetailType

SalesItemLineDetail, GroupLineDetail, DescriptionOnly, DiscountLine, SubTotalLine

Native

Sublist columns

Tax mode
Native

LineAmountTypes

Exclusive / Inclusive / NoTax, per document

Partial

GlobalTaxCalculation

TaxExcluded / TaxInclusive / NotApplicable — non-US only

Native

Per-line taxcode

Driven by nexus configuration

Dimensions
Partial

Tracking[] per line

At most two categories

Partial

ClassRef (line or header), DepartmentRef (header)

Placement depends on a preference

Native

department, class, location, cseg_*

Header and line; line wins

Settlement links
Native

Payments[], CreditNotes[], Prepayments[], Overpayments[]

Inline on the invoice

Partial

LinkedTxn[]

Ids only; fetch the payment to see the amount

Native

Transaction relationships

Foreign currency
Native

CurrencyCode + CurrencyRate

Native

CurrencyRef + ExchangeRate

Native

currency + exchangerate

Plus per-book and consolidated rates

Custom fields
Absent

—

Reference / URL fields only

Partial

CustomField[]

Three, defined per company

Native

custbody_*, custcol_*

Unlimited, typed, first-class

What actually differs, and what you do about it

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.

Xero — account-based line, tracking on the line

Xero
{
  "Type": "ACCREC",
  "Contact": { "ContactID": "0c1f…" },
  "Date": "2026-09-07",
  "DueDate": "2026-10-07",
  "LineAmountTypes": "Exclusive",
  "LineItems": [{
    "Description": "Consulting",
    "Quantity": 1,
    "UnitAmount": 1000,
    "AccountCode": "200",
    "TaxType": "OUTPUT2",
    "Tracking": [{ "Name": "Department", "Option": "R&D" }]
  }]
}

QBO (non-US) — item-based line, numeric tax code, department on the header

QBO
{
  "CustomerRef": { "value": "58" },
  "TxnDate": "2026-09-07",
  "DueDate": "2026-10-07",
  "GlobalTaxCalculation": "TaxExcluded",
  "DepartmentRef": { "value": "3" },
  "Line": [{
    "DetailType": "SalesItemLineDetail",
    "Amount": 1000,
    "SalesItemLineDetail": {
      "ItemRef": { "value": "12" },
      "Qty": 1,
      "UnitPrice": 1000,
      "TaxCodeRef": { "value": "8" }
    }
  }]
}

NetSuite — item sublist, segments per line, subsidiary on the header

NetSuite
{
  "entity": { "id": "58" },
  "trandate": "2026-09-07",
  "duedate": "2026-10-07",
  "subsidiary": { "id": "2" },
  "item": { "items": [{
    "item": { "id": "12" },
    "quantity": 1,
    "rate": 1000,
    "taxcode": { "id": "5" },
    "department": { "id": "7" },
    "cseg_product": { "id": "3" }
  }] }
}

The type the components consume

invoices.ts
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.

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.