Skip to content

Reading, paging and delta sync

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.

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
Reading, paging and delta sync fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Page mechanism
Native

page + pageSize

Journals use offset by JournalNumber

Native

STARTPOSITION + MAXRESULTS

In the query string

Native

limit + offset

With hasMore / links

Page size
Partial

100 default, up to 1,000

Where pageSize is supported. Verify per endpoint

Native

Up to 1,000 rows

/cdc: 1,000 objects per entity per response

Native

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
Partial

where= expression

Limited string operations, no joins

Partial

SQL-like WHERE and ORDER BY

No joins, no aggregates

Native

SuiteQL

Joins, aggregates, near-full SQL

What a list returnsThe row that decides whether a NetSuite sync completes.
Native

Full records

Native

Full records

Partial

REST: ids and links only

SuiteQL returns rows

Delta sync
Native

If-Modified-Since header

UTC, whole-record granularity

Native

MetaData.LastUpdatedTime, or /cdc

Native

lastmodifieddate in SuiteQL

Are deletes visible?
Native

Yes

Status becomes DELETED or VOIDED; the record persists

Partial

Via /cdc only

30-day lookback, and not for TaxCode, TaxRate, TaxAgency, TimeActivity or JournalCode; plain queries never show them

Absent

No

Hard deletes simply stop appearing

Backfill cost
Partial

Bounded by the daily cap

A large org is a multi-day, resumable job, and paid tiers meter egress

Native

Fast

1,000 rows per call, 500 calls per minute — but reads are metered monthly by App Partner tier

Partial

Bounded by concurrency

SuiteQL makes it feasible; REST does not

Ordering guarantee
Native

Journals are strictly sequential

Other endpoints: order by UpdatedDateUTC

Native

ORDER BY on the query

Native

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.

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 docsVerify quantitative claims

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.