How do you get everything out, and then keep it fresh? Xero pages 100 records at a time against a daily cap of 5,000 calls per organisation. QuickBooks queries 1,000 rows with a SQL-like syntax. NetSuite's REST list endpoints return ids only — use SuiteQL, or the sync will not finish.
Concepts
8
Divergences
4
Structural
2
Volatility
Verify the numbers
Endpoints
Xero
?page=1&pageSize=1000, If-Modified-Since
Journals page by offset instead
QBO
STARTPOSITION / MAXRESULTS, /cdc
Change data capture for deltas, 30-day lookback
NetSuite
SuiteQL with limit / offset
REST list endpoints return 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
Reading, paging and delta sync fields compared across Xero, QuickBooks Online and NetSuite
Concept
Xero
QBO
NetSuite
Page mechanism
page + pageSize
Journals use offset by JournalNumber
STARTPOSITION + MAXRESULTS
In the query string
limit + offset
With hasMore / links
Page size
100 default, up to 1,000
Where pageSize is supported. Verify per endpoint
Up to 1,000 rows
/cdc: 1,000 objects per entity per response
Up to 1,000
Per page; a SuiteQL query stops at 100,000 rows in total, so slice big reads by date or id
Filtering
where= expression
Limited string operations, no joins
SQL-like WHERE and ORDER BY
No joins, no aggregates
SuiteQL
Joins, aggregates, near-full SQL
What a list returnsThe row that decides whether a NetSuite sync completes.
Full records
Full records
REST: ids and links only
SuiteQL returns rows
Delta sync
If-Modified-Since header
UTC, whole-record granularity
MetaData.LastUpdatedTime, or /cdc
lastmodifieddate in SuiteQL
Are deletes visible?
Yes
Status becomes DELETED or VOIDED; the record persists
Via /cdc only
30-day lookback, and not for TaxCode, TaxRate, TaxAgency, TimeActivity or JournalCode; plain queries never show them
No
Hard deletes simply stop appearing
Backfill cost
Bounded by the daily cap
A large org is a multi-day, resumable job, and paid tiers meter egress
Fast
1,000 rows per call, 500 calls per minute — but reads are metered monthly by App Partner tier
Bounded by concurrency
SuiteQL makes it feasible; REST does not
Ordering guarantee
Journals are strictly sequential
Other endpoints: order by UpdatedDateUTC
ORDER BY on the query
ORDER BY in SuiteQL
What actually differs, and what you do about it
A difference stated without a response is trivia. Each of these carries the handling.
Structural
NetSuite REST list endpoints return ids, not records
NetSuite
GET /record/v1/invoice returns a page of { id, links } objects. Fetching 5,000 invoices that way is 5,001 calls against a concurrency limit in the single digits. This is not an optimisation problem; it is the difference between a sync that finishes and one that does not.
Do — Use SuiteQL for every bulk read. Reserve the REST record service for single-record reads and for writes.
Structural
Deletes are invisible in two of the three
QBO · NetSuite
Xero never truly deletes — it flips a status, so a delta sync sees it. QBO surfaces deletes only through change data capture, which looks back 30 days at most, so a poll-only integration — or one that was offline for a month — accumulates ghost records forever. NetSuite hard-deletes and the record simply stops existing.
Do — Poll for changes, but reconcile the id set periodically: fetch all ids for a window and mark anything missing as gone. Without that pass, aging reports quietly include documents that no longer exist.
Sharp edge
Xero's daily cap makes backfill a scheduling problem
Xero
Xero allows 5,000 calls per organisation per day (1,000 on the free Starter tier), 60 per minute and five in flight. A full history for a large organisation cannot be fetched in one day. Any sync that assumes it can complete in one run will fail partway through and, if it is not resumable, start again from the beginning tomorrow. Paid tiers also meter monthly egress — the bytes your GETs download — so re-reading unchanged history now costs money as well as time.
Do — Persist a cursor per endpoint — the Journals offset, the highest UpdatedDateUTC — and resume from it. Show sync progress to the user as a real, multi-day state.
Papercut
Modified-since is whole-record, not field-level
Xero · QBO · NetSuite
All three tell you a record changed and none tells you what changed. A trivial edit re-delivers the whole document, and a sync that recomputes downstream state on every delivery does a great deal of unnecessary work.
Do — Hash the normalised record and skip downstream work when the hash is unchanged. It also gives you the before-and-after that QBO's missing audit trail does not.
The same fact, different payloads
Stacked rather than tabbed — a reader comparing two shapes should not have to hold one of them in memory.
QBO — paged query, then change data capture for deltas
QBO
GET /v3/company/{realmId}/query?query=
SELECT * FROM Invoice
WHERE MetaData.LastUpdatedTime > '2026-09-01T00:00:00-07:00'
ORDER BY MetaData.LastUpdatedTime
STARTPOSITION 1 MAXRESULTS 1000
GET /v3/company/{realmId}/cdc
?entities=Invoice,Bill,Payment&changedSince=2026-09-01T00:00:00-07:00
// /cdc is the only place a delete appears.
Xero — the GL feed, resumable by construction
Xero
GET /api.xro/2.0/Journals?offset=4820
Xero-Tenant-Id: 0c1f…
// Returns up to 100 journals with JournalNumber > 4820.
// Store the highest JournalNumber seen. It never goes backwards,
// so the cursor is exact and a crash costs at most one page.
NetSuite — SuiteQL, because the REST list gives you ids
NetSuite
POST /services/rest/query/v1/suiteql
Prefer: transient
{ "q": "SELECT id, tranid, trandate, entity, foreignamount, status
FROM transaction
WHERE type = 'CustInvc'
AND lastmodifieddate >= TO_DATE('2026-09-01','YYYY-MM-DD')
ORDER BY id" }
// limit and offset are query parameters, max 1000 per page.
// A query stops at 100,000 rows: slice large reads by date or by id.
What this changes in the interface
The payoff. Every difference above that reaches the surface, and what the component does about it.
Sync can take days
Sync state is a first-class object in the interface: last completed, in progress with a real position, throttled, failed and resumable. Never an indeterminate spinner.
Data has an age
Every figure sourced from a mirror carries an as-of time. 'Synced 4 minutes ago' is content, not chrome.
Ghost records are possible
Reconciliation passes are surfaced: when a document disappears from the source, the UI says it was removed and when we noticed, rather than deleting it silently.
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.