Skip to content

Webhooks and freshness

How do you find out something changed? Xero notifies you about four categories — contacts, invoices, credit notes and subscriptions. QuickBooks notifies you about most. NetSuite does not notify you at all. None of them sends the data — every notification is an instruction to go and fetch.

Concepts
7
Divergences
3
Structural
1
Volatility
Verify the numbers

Endpoints

Xero

Webhooks: Invoices, Contacts, Credit notes, Subscriptions

x-xero-signature, HMAC-SHA256

QBO

Webhooks across most entities

intuit-signature, HMAC-SHA256

NetSuite

None

User event script callout, or poll

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
Webhooks and freshness fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Available at all
Partial

Four event categories

Invoices, Contacts, Credit notes (added 2026), and app Subscriptions

Native

Most entities

Absent

None

Script it yourself

PayloadNone of the three sends the record.
Partial

Resource ids and event type

Partial

Entity name, id and operation

CloudEvents envelope (e.g. qbo.invoice.updated.v1) — mandatory migration from the legacy eventNotifications shape by 15 May 2026

Native

Whatever your script sends

Signature
Native

x-xero-signature

HMAC-SHA256 over the raw body

Native

intuit-signature

HMAC-SHA256

Partial

Yours to design

Endpoint validation
Native

Intent to receive

Fail it and the endpoint is disabled

Native

Verifier token

Absent

—

Delivery guarantee
Partial

Retries, then disables

Native

Retries with backoff

Partial

Yours

Deletes notified
Partial

As an update to a voided record

Native

Operation: Delete / Void / Merge

Absent

Only if you script it

Practical freshness
Partial

Seconds for the notified entities; your poll interval for the rest

Native

Seconds

Partial

As often as you poll

What actually differs, and what you do about it

A difference stated without a response is trivia. Each of these carries the handling.

Sharp edge

Every webhook is a notification, not a feed

Xero · QBO

You receive 'invoice X changed' and must fetch the invoice. That fetch counts against the same rate limit as everything else, so a burst of webhooks during a bulk edit in the ledger's own UI can exhaust your minute budget and delay everything else.

Do — Enqueue notifications, coalesce duplicates for the same record, and fetch through the same limiter as the rest of the sync. Never fetch inline in the webhook handler — return 2xx first.

Sharp edge

Xero disables an endpoint that fails validation

Xero

The intent-to-receive handshake must succeed before any business logic runs: Xero sends signed and deliberately mis-signed requests and expects 200 and 401 respectively, within five seconds. A slow or failing endpoint gets turned off rather than retried indefinitely. A deployment that breaks the signature check silently stops all notifications.

Do — Verify the signature and return 2xx within the timeout, before any processing. Monitor for the absence of webhooks as an alarm condition — silence looks identical to nothing happening.

Structural

NetSuite has no webhooks

NetSuite

The substitutes are a user event SuiteScript that calls out on save — which puts your endpoint's availability in the path of the customer's ability to save a record — or scheduled polling on lastmodifieddate.

Do — Poll by default. Offer the script only where the customer's own team maintains it, and never let a callout block a save.

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 — verify first, enqueue second, fetch later

Xero
const expected = createHmac("sha256", XERO_WEBHOOK_KEY)
  .update(rawBody, "utf8").digest("base64");

// Constant-time compare, before anything else touches the body.
if (!timingSafeEqual(Buffer.from(expected),
                     Buffer.from(req.headers["x-xero-signature"]))) {
  return new Response(null, { status: 401 });
}

await queue.push(JSON.parse(rawBody).events);   // ids only
return new Response(null, { status: 200 });     // within the timeout

What this changes in the interface

The payoff. Every difference above that reaches the surface, and what the component does about it.

Freshness varies by source and by entity
The connection panel states the mechanism and the age per source: webhooks for QBO invoices, hourly polling for NetSuite. Users plan around it when they can see it.
Notifications can stop silently
The absence of events is monitored and surfaced. A connection that has heard nothing for longer than expected shows as stale, not as healthy.

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.