How to integrate with Xero: One object sends events, the other nine you poll
Xero is a pleasant API to write against and a demanding one to keep in sync. Ten objects are readable, exactly one of them announces its own changes, and the filter Xero recommends for the rest is documented to miss certain updates.


Your app, your tenants and the tier that decides both
Xero is a cloud accounting platform for small businesses and accounting practices, strongest in the UK, Australia, New Zealand, the USA and Canada. The API is well documented and predictable. What decides your build is not the surface, it is how many customers you are allowed to attach to it.
The app is registered by you, in your name, and it is the app that carries the connection budget. If you want the registration steps, the object matrix and the partner tiers in one place, that is the Xero API page. What follows is what the platform does once you are connected.
Xero used to allow 25 connections to an uncertified app. That model is gone. Xero, verbatim: “Effective March 2026, we have shifted to a tiered pricing model based on connections and API usage. To be listed on the Xero App Store, you will need to be in the Plus tier or higher.”
A new app now defaults to the Starter tier with five connections, and Core raises it to fifty. The one limit that survived the change is the other one: an organisation or practice can connect at most two uncertified apps at a time.
The quotation is from Xero’s certification checkpoints. The same tier figures appear independently on OAuth 2.0 API limits and Xero tenants. Checked 10 August 2026.
A connection is a link between your app and one tenant, created per unique user and tenant pair. For the limit, though, Xero counts tenants rather than pairs. Their example: three users on one organisation and one user on another are four connections and two against your ceiling.
Connections then stay alive on their own. Xero keeps them “active indefinitely even if no API calls are being made or if the access token expires”, and only six named events remove one: the user disconnects in Xero, the user loses permission, you delete it through the connections endpoint, the token is revoked, the tenant is deleted or you delete your app.
A tenant ID travels with every single request, and Maesn resolves it at authentication and attaches it afterwards. Xero also needs no environment or company picker, so it runs the plain consent flow rather than the interactive one that Business Central and the DATEV systems use.
Connection counting and lifetime from Xero tenants, checked 10 August 2026. The list of systems on the interactive flow is in Maesn authentication, and Xero is not on it.
Thirty minutes, sixty days and one grace period
Xero access tokens live 30 minutes and refresh tokens 60 days. That much is widely known. The part that costs people a customer is what happens when a refresh goes wrong.
Every refresh returns a new access token and a new refresh token, and you have to persist both. If the response never reaches you, Xero keeps the old refresh token usable for a while, and that window is short and finite.
POST /connect/token HTTP/1.1Host: identity.xero.comContent-Type: application/x-www-form-urlencodedgrant_type=refresh_token&refresh_token=<your stored refresh token># 200 returns a NEW access token and a NEW refresh token.# Persist both before you acknowledge anything upstream.## Response lost? The previous refresh token still works,# but only for 30 minutes. After that the customer has to# authorise your app again.
Xero, verbatim: “you can retry using your existing refresh token for a grace period of 30 minutes. After 30 minutes your previous refresh token will expire and the user will need to re-authorise your api application” (the code flow, checked 10 August 2026).
So the failure mode is not an expired token, it is a lost write. A process that crashes between receiving the new pair and storing it has half an hour to notice, and after that the only fix is a human clicking through consent again. That is why the refresh cycle is the first thing worth moving out of your own deployment path.
Through Maesn the cycle runs per connected organisation on our side, and what your code holds instead is an account key. How that generalises across systems that all expire differently is unified authentication.
One detail to keep even if you never touch OAuth again: the offline_access scope is not optional. Xero requires it for certification, and without it a connection cannot outlive the first 30 minutes.
One of the ten readable objects sends an event
This is the sentence the rest of the architecture follows from. Through Maesn, Xero exposes ten readable objects, and exactly one of them announces its own changes: invoices. The second event category, contacts, sits on an object that has no read path at all.
That combination is not a quirk of one matrix row. Across all 30+ systems we support, eleven objects send events without being readable by default, and ten of those are marked available on request. Xero’s contacts is the only one where the read simply is not there.
| Xero category | Through Maesn |
|---|---|
| Invoice, create and update | Enabled. Invoices are readable, so the follow-up call is obvious |
| Contact, create and update | Enabled. Contacts is writable but not readable, so the follow-up call goes to customers or suppliers |
| Credit note, create and update | Available on request, like the rest of the credit note row |
| Subscription, create and update | Not mapped. It reports App Store billing for your own listing rather than accounting data |
Categories and event types from Xero webhooks, the enabled resources from the Maesn Xero documentation, the comparison across all 30+ systems counted from the generated coverage data. Checked 10 August 2026.
No read path of its own. The record is reachable under two other names, and the event does not say which one applies.
A contact is written under one name and read under two. Creating and updating go to contacts, reading goes to customers or suppliers.
The reason is that Xero keeps one contact record with flags on it, while the unified model splits the read into customers and suppliers. Fourteen of the 30+ systems draw that same line, so the split itself is ordinary. Xero is the only one of them that also fires an event on the combined name.
Two practical consequences. Archiving is not a delete: Xero folds it into update, “including when contacts are archived”, and there are no delete events at all. And the payload you receive is normalised, which is where the system-by-system differences become visible as empty fields rather than as branching code.
[{"eventType": "CREATED","filterDate": null,"resource": "CONTACT","resourceId": "b15311cf-c0c1-46fe-b895-341b4b2eb005","userId": "1cb1fbcc-b9ed-4ed8-a8c2-40a7fa5c8047"}]# filterDate stays null because Xero sends no date filter.# userId is populated, which is not true on every system.# Contacts has no read, so the second call is one of these:# GET /customers/{resourceId}# GET /suppliers/{resourceId}
Payload shape from the Maesn Xero documentation, where the comment on the field reads //not supported by xero. The documentation spells the key recourceId; that is a typo we had confirmed, and the field is resourceId. Checked 10 August 2026.
Lexware Office leaves filterDate and userId empty, Exact Online leaves both empty, Xero fills userId only, and Business Central fills filterDate but only on collection events. One envelope, and the gaps in it are where the platforms actually differ. That normalisation is unified webhooks.
What your endpoint has to answer within five seconds
Xero delivers webhooks to the app rather than to the customer, so one endpoint receives events for every organisation connected to you. Before it receives anything, it has to pass a validation Xero calls intent to receive, and the rules are unusually specific.
| Condition | Detail |
|---|---|
| HTTPS on port 443 | The standard port, not a custom one |
| A 2xx within 5 seconds | Acknowledge first and process afterwards, or you will miss it |
401 on a bad signature | During validation Xero deliberately sends a wrongly signed payload and expects to be rejected |
| No cookies in the response | Xero lists this explicitly, and a session middleware that sets one will fail you |
All four from Xero webhooks, checked 10 August 2026. The signature is HMACSHA256 over the payload with your webhook signing key, base64 encoded, in the x-xero-signature header.
The 401 is the part that trips people, because it is the one response this endpoint has to get wrong on purpose. Xero validates that you reject forgeries before it trusts you with real ones, and an endpoint that cheerfully answers 200 to everything never leaves the validation state.
What happens after a failure is more forgiving than most platforms. Xero retries immediately, then every 15 minutes for up to 24 hours, then disables the subscription and emails the collaborators on your app. Events that occurred meanwhile are held for 31 days and replayed “in order” once you are healthy again.
Which is exactly why Xero also writes that consumers “must ensure they implement idempotency logic and support message replayability”. A replay of 31 days of events is a feature only if the second delivery of an event changes nothing.
Every delivery carries firstEventSequence and lastEventSequence alongside the events array. If you persist the last sequence you processed, a replayed batch is cheap to recognise, and a gap is visible rather than silent.
Nine objects without an event, and a budget of five thousand
Nine of the ten readable objects never tell you they changed. Accounts, booking proposals, customers, expenses, invoice lines, journal entries, suppliers, tax rates and users are all things you have to ask about, and Xero meters the asking.
| Limit | Ceiling | What comes back |
|---|---|---|
| Concurrent | 5 calls in progress | 429 with no Retry-After |
| Per minute | 60 per tenant | 429 with Retry-After |
| Per day | 5.000 per tenant, 1.000 on Starter | 429 with Retry-After |
| Per app | 10.000 a minute, all tenants | 429, and it is not one customer’s fault |
From OAuth 2.0 API limits, checked 10 August 2026. Xero writes that the minute and daily limits “also” return Retry-After, which is what leaves the concurrency case without one. Every response carries X-MinLimit-Remaining, X-DayLimit-Remaining and X-AppMinLimit-Remaining.
A 429 alone does not say which of the four limits fired. X-Rate-Limit-Problem does, and it is the only one that always arrives. Backoff logic built solely on Retry-After has nothing to wait on when the cause is the five concurrent calls, which is the limit a parallel sync hits first.
Xero prices polling itself, and the figure is theirs: “if your rate limit is 5000 calls per day per organisation and you poll just one endpoint every 5 minutes for a single organisation, you’ll burn through 288 calls per day”. They add that excessive polling is “the most common cause of exceeding limits and being temporarily blocked”.
Put the two numbers together and the day has a shape. Nine objects at five-minute intervals is 2.592 calls, more than half the budget, spent before a single record has been read or a single invoice written.
- 2.592
- asking. Nine objects without an event, each checked every five minutes.
- 2.408
- reading. What is left for the records themselves, plus every write.
The 288 is Xero’s own figure. One endpoint polled every five minutes costs 288 calls a day, and nine of them is arithmetic on top of it.
Organisations on Xero’s Early plan may approve 20 sales invoices and 5 bills a month. Exceeding that returns an HTTP 400 carrying “You have reached the limit of invoices you can approve”. It reads as a validation error because that is what it is, and error handling keyed purely on status codes will file it under bad payload. How that class is normalised is unified error handling.
The delta filter, and the changes it does not report
The documented answer to a polling budget is to ask smaller questions. If-Modified-Since takes a UTC timestamp accurate to the second and returns only what changed, and Xero supports it on fourteen endpoints including invoices, contacts, payments, journals and purchase orders.
It is the right tool. It also has a hole in it, and the useful thing is that Xero says so on the same page rather than leaving you to find out.
“Not all changes will trigger a change of the UpdatedDateUTC field. These include changes to partially paid transactions which don’t generate a journal such as DueDate or SentToContact, and Contact fields pulled from other sources such as Balances, IsSupplier, and isCustomer. As a result, transactions with these changes may not be returned with an If-Modified-Since query.”
From HTTP requests and responses, checked 10 August 2026. The list of supported endpoints is on if-modified-since, the polling figures on reducing polling.
Read that list against what a finance product actually watches. A moved DueDate is the single most important field in dunning, and a partially paid invoice is exactly the case where it moves. SentToContact decides whether a reminder has gone out. IsSupplier and IsCustomer decide which side of the ledger a company belongs on.
None of those are edge cases, and none of them are covered by the two event categories either. So the honest architecture on Xero is three-legged: events where they exist, delta queries for the bulk, and a slower full pass for the fields the delta query is documented to miss.
Paging is the pleasant part by comparison. Nine endpoints support it, the default page size is 100 and the maximum 1.000, and out-of-range values are clamped rather than rejected.
The response carries a pagination object with pageCount and itemCount, so you no longer fetch until a page comes back empty. Everything else returns its whole set at once, which is two retrieval strategies in one interface and precisely the difference unified pagination and filtering removes.
The limits page lists five paginated endpoints, the requests page lists nine and adds payments, purchase orders, prepayments and overpayments. The nine is the current one. Worth knowing before you conclude an endpoint cannot page because the first page you landed on did not mention it.
A PUT creates, a POST upserts and a 200 can carry failures
Writing to Xero is where a shared data model earns its keep, because three of its conventions are the opposite of what a REST client expects. The first is the verbs. Xero, verbatim: “A PUT method will create new data in Xero, whereas a POST will either create new data or update existing data”.
The second is bulk. You can submit many entities in one call, which is how you stay inside the daily budget, and Xero recommends batches of about 50. But the flag that makes bulk usable also changes what a status code means.
POST /api.xro/2.0/Invoices?summarizeErrors=falseAuthorization: Bearer <access token>Xero-tenant-id: <tenant id>Accept: application/jsonIdempotency-Key: <one key per logical write># 200 OK, and the outcome is per entity, not per request:# "StatusAttributeString": "OK"# "StatusAttributeString": "WARNING" + Warnings[]# "StatusAttributeString": "ERROR" + ValidationErrors[]
Xero on the flag: “you will receive a HTTP 200 status code (rather than HTTP 400) if you are using the summarizeErrors parameter” (HTTP requests and responses, checked 10 August 2026).
So a green status code proves the request was accepted and nothing about whether the records landed. Xero is also blunt about the middle state: “Do not suppress or ignore warnings”, because a warning means the write succeeded while the data may still be wrong, and wrong data in a ledger is a reporting problem later rather than an error now.
The third convention is the good one. Xero has real idempotency: an Idempotency-Key header on POST, PUT and PATCH, ignored elsewhere, with the first response cached and returned for repeats. The window is six minutes from the first call, so it covers a transient network retry and deliberately not a nightly rerun.
Key handling, the six-minute lifetime and the recommendation to build keys from four concatenated UUIDs are from idempotent requests, checked 10 August 2026. Reusing one key with a different body is an error rather than a silent overwrite.
“By default all successful responses on the accounting API are returned as XML”, and JSON needs an Accept header. When you get JSON, dates arrive as "/Date(1439434356790)/", a millisecond Unix timestamp in a .NET wrapper that Xero calls “ugly” and cannot remove without a breaking change. Turning that into one date type across every system is what the common data model does.
What stays on your side: The 429, the second read and the app
We absorb the shape of the integration, not the rules of the platform. Four things stay with you, and it is cheaper to know that now than after the first customer call.
- The rate limit still reaches you. Only Maesn’s asynchronous endpoints absorb bursts, so Xero’s
429arrives in your code and the backoff is yours. What the asynchronous path does and does not cover is asynchronous processing. - The read after a contact event is a decision. We hand you the event with its resource id. Because contacts has no read of its own, choosing customers or suppliers depends on what that record is in your product, and only you know that.
- The app is yours, so the tier is yours. You register it in Xero’s developer portal under your product name, which is the name on your customer’s consent screen, and the connection ceiling and certification belong to that app. We run the partner process with you and we cannot hold it for you.
- Deletes do not exist here. No object on this system has delete enabled and Xero sends no delete events, so corrections are posted as new documents. Build that path rather than planning around it.
Xero offers a free trial organisation, and it is also where you register your app, so a development account costs nothing and behaves like the real thing. Maesn additionally runs sandboxes for every supported system, Xero included, populated with test data on request. Neither removes the certification step before you can scale past the Starter tier.
Sandbox availability from Maesn sandboxes, which lists Xero with both a free trial and a Maesn-provided environment. Checked 10 August 2026.
Frequently asked questions
How many Xero organisations can connect to an uncertified app?
What happens if a Xero token refresh fails halfway?
Which Xero objects send webhooks?
Does Xero always send Retry-After with a 429?
Why does If-Modified-Since miss some Xero changes?
Why did my Xero bulk POST return 200 with failed records?
Does Maesn absorb Xero's rate limit?

QuickBooks Online Webhooks: Events, Retries and Recovery
QuickBooks Online webhooks cover 29 entity types and expect HTTP 200 in three seconds. Why Intuit still asks you to poll change data capture.
Lennart Svensson · 25 Aug 2026
Lexware Office Pagination: The 406 and One Page Size
Lexware Office validates the page size and rejects a bad one with 406, the same code it uses for an unsupported media type. What that means for your read loop.
Lennart Svensson · 20 Aug 2026
How to Integrate with DATEV Rechnungswesen: One Connection
One connection carries reading and writing in DATEV Rechnungswesen, on a two-year token. Which objects travel in which direction is the real decision.
Lennart Svensson · 17 Aug 2026Build once on the Unified API.
Token cycles, event gaps, delta filters with holes in them and per-platform write verbs are the same problem on every accounting system. Solve them once with Maesn instead of once per platform.