What happens when you write, and how fast can you go? QuickBooks replaces the whole entity unless you ask it not to. Xero caps you by the day. NetSuite governs by concurrency and may reject a write for reasons that are not in your payload at all.
Concepts
10
Divergences
4
Structural
2
Volatility
Verify the numbers
Endpoints
Xero
POST /Invoices with Idempotency-Key
Arrays of up to 50 objects per request
QBO
POST /invoice?requestid=…, POST /batch
Up to 30 operations per batch; requestid for idempotency
NetSuite
PATCH /record/v1/{type}/{id}
Scripts and workflows fire on write
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
Writes, concurrency and rate limits fields compared across Xero, QuickBooks Online and NetSuite
Concept
Xero
QBO
NetSuite
Update semanticsThe most dangerous single row in these three APIs.
Merge
Supplied fields update; arrays replace wholesale
Replace
The full entity is overwritten unless "sparse": true
Patch
Supplied fields update
Concurrency control
Last write wins
SyncToken
Must round-trip; a mismatch is a 400
Record-level
Scripts may reject
Idempotency
Idempotency-Key header
On write endpoints
requestid query parameter
Unique per company, ≤50 characters (36 in batch); a repeat returns the original response
None
Dedupe yourself on externalid or a reference field
Batch
Arrays of up to 50 objects
Per POST, per endpoint; Xero's guidance is 50 per request
/batch, up to 30 operations
Batch calls throttled at 40 per minute per realm
Not on REST record
SuiteScript for bulk
Partial failure
SummarizeErrors=false
Per-object validation results
Per-operation results in the batch response
Per call
Delete
Status change
VOIDED or DELETED; the record persists
?operation=delete or ?operation=void
DELETE
Real, where the period and permissions allow
Rate limit
60/min and 5,000/day per organisation
1,000/day on the free Starter tier; 10,000/min across the whole app
500 requests/min per realm
Plus monthly metering of reads (CorePlus) by App Partner tier; Builder is blocked above 500,000
Governed by concurrency, not rate
Account limit by service tier — 5 on Standard, 15 on Premium — plus 10 per SuiteCloud Plus licence
Concurrency
5 concurrent per organisation
10 concurrent per realm
Tier-dependent
RESTlets also consume SuiteScript usage units
On breach
429 with Retry-After
X-Rate-Limit-Problem names the limit; X-MinLimit-Remaining / X-DayLimit-Remaining warn first
429 ThrottleExceeded
Intuit says wait 60 seconds
429 / SSS_REQUEST_LIMIT_EXCEEDED
Server-side logic on write
None
Tax engine only
SuiteScript, SuiteFlow, custom validation
A write enters a business process
What actually differs, and what you do about it
A difference stated without a response is trivia. Each of these carries the handling.
Structural
A QuickBooks update deletes the lines you omit
QBO
POST an Invoice with only the fields you meant to change and every line on it disappears. The call succeeds. The invoice is now empty and the customer's balance has moved.
Do — Read the current record, mutate it, write the whole thing back with the current SyncToken — or use sparse updates and accept their narrower semantics. A SyncToken mismatch is a real user-visible outcome, not a retryable error: someone else changed the record.
Structural
NetSuite writes can fail for reasons you cannot see
NetSuite
A user event script, a workflow condition, a mandatory custom field or a subsidiary restriction can reject a payload that is structurally perfect. The error text often names an internal field id and nothing else.
Do — Surface the raw NetSuite error to the operator, not a rewritten message. Keep the payload alongside it so support can reproduce. Never retry a rejection automatically.
Sharp edge
The rate limits differ in shape, not just in size
Xero · QBO · NetSuite
Xero's daily cap (5,000 per organisation, 1,000 on Starter) makes backfill a multi-day job. NetSuite's concurrency governance means parallelism is capped hard and two workers each politely using three connections will trip a five-connection limit. QBO's 500-a-minute rate is the most forgiving of the three per minute — but its reads are now counted per month against a partner-tier allowance.
Do — One token-bucket limiter per source and tenant, shared across every worker, with retries flowing through it. Retry 429 and 5xx with exponential backoff and jitter; never retry a 400.
Sharp edge
Idempotency is opt-in on two and absent on NetSuite
QBO · NetSuite
A timeout on a write leaves you unsure whether the document exists. Xero honours an Idempotency-Key header, and QBO a requestid query parameter — unique per company, up to 50 characters (36 in a batch) — that returns the original response instead of writing twice. Both only work if you send them. NetSuite's REST record service has neither, so a blind retry may create a duplicate invoice with a new number, which someone will eventually pay.
Do — Send the Xero key and the QBO requestid on every write, generated once per user action and reused on retry. For NetSuite, write a client-generated reference into a field it stores (tranid, externalid, a custom field) before the call, and search on it after any ambiguous failure instead of retrying blind.
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 — the read-mutate-write cycle, in full
QBO
// 1. Read. You need the SyncToken and every line you intend to keep.
const { Invoice } = await get(`/invoice/${id}`);
// 2. Mutate the whole object.
const next = { ...Invoice, DueDate: "2026-11-07" };
// 3. Write it all back. Omitting Line here would empty the invoice.
await post("/invoice", next); // SyncToken travels inside `next`
// The alternative, with narrower semantics:
await post("/invoice", { Id: id, SyncToken: Invoice.SyncToken, sparse: true,
DueDate: "2026-11-07" });
Xero — idempotent write, per-object errors
Xero
POST /api.xro/2.0/Invoices?summarizeErrors=false
Xero-Tenant-Id: 0c1f…
Idempotency-Key: inv-batch-2026-09-07-001
{ "Invoices": [ /* up to 50 objects */ ] }
// With summarizeErrors=false, each object comes back with its own
// ValidationErrors. One bad invoice does not reject the batch.
What this changes in the interface
The payoff. Every difference above that reaches the surface, and what the component does about it.
A write can conflict
Optimistic UI must be able to unwind. A SyncToken conflict is presented as 'someone else changed this', with a diff, not as a generic failure.
Some writes are not idempotent
Submit controls are single-shot with a client reference, and an ambiguous failure offers 'check whether it was created', not 'try again'.
Throttling is normal, not exceptional
A queued write says it is queued, with a position. Rate limiting is a visible state in the interface, not an error.
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.