Skip to content

Writes, concurrency and rate limits

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.

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
Writes, concurrency and rate limits fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Update semanticsThe most dangerous single row in these three APIs.
Native

Merge

Supplied fields update; arrays replace wholesale

Partial

Replace

The full entity is overwritten unless "sparse": true

Native

Patch

Supplied fields update

Concurrency control
Absent

Last write wins

Native

SyncToken

Must round-trip; a mismatch is a 400

Partial

Record-level

Scripts may reject

Idempotency
Native

Idempotency-Key header

On write endpoints

Native

requestid query parameter

Unique per company, ≤50 characters (36 in batch); a repeat returns the original response

Absent

None

Dedupe yourself on externalid or a reference field

Batch
Native

Arrays of up to 50 objects

Per POST, per endpoint; Xero's guidance is 50 per request

Native

/batch, up to 30 operations

Batch calls throttled at 40 per minute per realm

Absent

Not on REST record

SuiteScript for bulk

Partial failure
Native

SummarizeErrors=false

Per-object validation results

Native

Per-operation results in the batch response

Partial

Per call

Delete
Partial

Status change

VOIDED or DELETED; the record persists

Native

?operation=delete or ?operation=void

Native

DELETE

Real, where the period and permissions allow

Rate limit
Partial

60/min and 5,000/day per organisation

1,000/day on the free Starter tier; 10,000/min across the whole app

Native

500 requests/min per realm

Plus monthly metering of reads (CorePlus) by App Partner tier; Builder is blocked above 500,000

Partial

Governed by concurrency, not rate

Account limit by service tier — 5 on Standard, 15 on Premium — plus 10 per SuiteCloud Plus licence

Concurrency
Partial

5 concurrent per organisation

Native

10 concurrent per realm

Partial

Tier-dependent

RESTlets also consume SuiteScript usage units

On breach
Native

429 with Retry-After

X-Rate-Limit-Problem names the limit; X-MinLimit-Remaining / X-DayLimit-Remaining warn first

Native

429 ThrottleExceeded

Intuit says wait 60 seconds

Native

429 / SSS_REQUEST_LIMIT_EXCEEDED

Server-side logic on write
Absent

None

Partial

Tax engine only

Native

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.

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.