
Xero API vs Database Sync: Which is Better?
Xero API vs database sync for getting accounting data into PostgreSQL: a build-vs-buy guide covering OAuth tokens, rate limits, and the real maintenance cost.
If you're building on top of Xero, you'll eventually need that accounting data in your own database, to power dashboards, reconcile revenue, or join it against your product tables. There are two ways to get it there: build your own integration against the Xero API, or hand the job to a managed database sync. Both work, but they cost you very different things.
It helps to be honest about the framing first. This isn't really "API versus no API", because a managed sync uses the Xero API under the hood too. The real question is build versus buy: do you own the OAuth tokens, the tenant routing, the pagination, the rate-limit backoff, and the schema mapping yourself, or do you let a service run all of that for you? This post compares the two at decision altitude. (If you just want the step-by-step setup instead, here is how to sync Xero to PostgreSQL in five minutes.)
How a Xero API Integration Works
Building your own integration means going directly to Xero's developer platform. The shape of the work is always the same:
- Register an app in the Xero Developer portal and get your client credentials.
- Send each customer through the OAuth 2.0 consent flow with the
offline_accessscope, then store the access and refresh tokens. Because one Xero login can cover several companies, you also call the Connections endpoint and keep track of eachtenantId. - Poll the Accounting API for the objects you care about (invoices, contacts, payments, and so on), paging through the results and sending the right
Xero-tenant-idheader on every call. - Map each object shape into your own tables, and keep refreshing tokens so the connection never goes stale.
In code, the loop you end up owning looks something like this:
// The moving parts you operate yourself, on every connected organisation, forever
const accessToken = await refreshIfExpired(org.tenantId); // 30-minute access tokens
const invoices = await pageThrough('/Invoices', accessToken, org.tenantId); // 100 per page
for (const invoice of invoices) {
await upsertIntoPostgres(invoice); // your mapping, your schema
}
// plus: refresh-token rotation, 429 backoff, per-tenant scheduling, and schema drift
That snippet hides a lot. The OAuth handshake, the tenant enumeration, the pagination, and the incremental fetching are each a small project of their own. The point here is narrower: what does that integration cost you after it ships?
The Hidden Costs of Rolling Your Own
The first version is the fun part. The cost shows up later, in the things you have to keep running:
OAuth tokens you babysit forever. Access tokens last 30 minutes, so you are refreshing constantly. The refresh token has a rolling 60-day life, but it is single-use: every refresh returns a brand new refresh token and invalidates the old one, so you must persist the newest value every single time. Store the wrong one and you get a dead connection that only a manual reconnect can fix. Let a connection sit idle past 60 days and it expires too.
Rate limits you engineer around. Xero throttles each connected organisation to 60 calls per minute, with only 5 requests allowed in flight at once and a further daily cap, on top of a 10,000-per-minute ceiling across your whole app. Cross a line and you get an HTTP 429 with a Retry-After header, then you back off and retry. With one client that is trivial. Across dozens of connected organisations, throttling becomes its own scheduling problem.
List calls hide the detail you actually need. Ask Xero for a list of invoices and it returns a trimmed summary with no line items, to keep the response fast. To get the full record you either fetch each invoice individually or page through with the pageSize parameter (100 per page by default, up to 1000). So a single "get my invoices" is rarely one call, it's many.
Incremental sync that quietly misses changes. The efficient way to pull only what changed is the If-Modified-Since header, which filters on each record's UpdatedDateUTC. The catch is that some changes never bump that timestamp, including a contact's outstanding balance and its customer or supplier flags. A naive incremental sync slowly drifts out of date, so you still need a periodic full reconciliation to stay honest.
Webhooks cover almost nothing. If you were hoping to skip polling, Xero webhooks only fire for four things: contacts, invoices, credit notes, and Xero App Store subscriptions. There is nothing for accounts, payments, bank transactions, items, or journals. They emit create and update events only (a deletion or void arrives, if at all, as an update), and the payload carries just a reference, the resource id and URL, not the data itself, so you still call the API to fetch the actual record. Undelivered events are retried and stored for around 31 days, which means idempotency and replay handling are yours to build.
Schema mapping across many objects, and it moves. Invoices, contacts, accounts, bank transactions, credit notes, items, purchase orders, and journals each carry their own shape and their own relationships. You map every one of them into tables, and when Xero changes a payload, you update the mapping.
How Database Sync Works
Database sync takes the opposite approach. Instead of you operating a pipeline against the Xero API, a managed service does the pulling and writes the results straight into your PostgreSQL database.
The flow looks like this:
- Connect your PostgreSQL database (Supabase, Neon, Railway, AWS RDS, or any PostgreSQL host)
- Authorize Xero once through the standard consent flow, and pick which organisation to sync
- The sync service calls the Xero API for you, pages through the data, and writes structured tables
- On every later run it pulls what changed and upserts, so there are no duplicates and no gaps
There is no Xero app to register, no refresh-token rotation to schedule, no 429 backoff to code, and no tenant headers to juggle. The accounting data simply shows up in your database, ready to query.
Side-by-Side Comparison
| Factor | Build It Yourself (Xero API) | Managed Database Sync |
|---|---|---|
| Time to first data | Days to weeks: app, OAuth, tenants, paging, schema | About 5 minutes: connect database, authorize |
| OAuth token lifecycle | Yours to run: 30-min access, 60-day rolling single-use refresh | Handled for you |
| Multi-organisation | You enumerate tenants and route every call | Pick an organisation and sync it |
| Rate-limit handling | You build it: 60/min per org, 5 concurrent, 429 backoff | Managed by the service |
| Historical backfill | You write paginated backfill | Full backfill on first sync |
| Incremental updates | You track UpdatedDateUTC and its blind spots | Pulls changes and upserts every run |
| Schema mapping | You map every object and track payload changes | Tables auto-created and maintained |
| Data freshness | On-demand; near real-time for the 4 webhook objects | Batch (scheduled, e.g. hourly or daily) |
| Control & customization | Full: fields, transforms, write-back | Standard tables, less custom |
| Total cost of ownership | Build plus perpetual maintenance | Predictable flat subscription |
When to Build It Yourself
Rolling your own is the right call in a few real cases:
- You need write-back. Sync tools are read-only by design. If you have to create or update records inside Xero, like raising invoices or adding contacts, you need the API directly.
- You need real-time reaction to one of the few events Xero actually pushes, such as a new invoice or contact, rather than a scheduled batch.
- You have unusual object or field requirements that a standard sync does not cover.
- You already run pipeline infrastructure, so the marginal cost of one more integration is genuinely low.
- You want full control over every transform and mapping, and you are willing to maintain it.
If that sounds like you, build it directly against the API and budget for the upkeep above. The control is real, and so is the maintenance.
When to Use Managed Sync
Managed sync is the right call when you mainly want to query the data, not operate a pipeline:
- Build dashboards and reporting on revenue, cash flow, or accounts payable and receivable
- Run ad-hoc SQL against your accounting data, like "which suppliers are we due to pay this week?"
- Join Xero data with your own tables, matching invoices or contacts to your app's users
- Keep maintenance near zero, with no token rotation, rate-limit code, or reconciliation job to own
- Pay a predictable flat cost instead of perpetual engineer time
Once your data is in Postgres, a question like "what are we owed, and what do we owe?" is just SQL:
-- Net position from xero_invoices: sales owed to you vs bills you owe
SELECT
type, -- ACCREC = sales invoices, ACCPAY = bills
COUNT(*) FILTER (WHERE amount_due > 0) AS open_invoices,
SUM(amount_due) AS outstanding
FROM xero_invoices
WHERE status IN ('AUTHORISED', 'SUBMITTED')
GROUP BY type;
Try doing that against the Xero API directly. You would need paginated calls per organisation, client-side filtering, and careful rate-limit handling. With synced data, it's one query. If you would rather not build any of the pipeline, here is how to sync Xero data to PostgreSQL automatically.
When to Use Both
These two approaches are not mutually exclusive, and the strongest setups use both:
- The Xero API (or a webhook) for the few moments you need to react instantly, like flagging a large invoice the second it's approved
- Managed sync for the queryable, reconciled copy your team runs reports against
Your app reacts to the events that genuinely need real-time handling, while your team runs any query it likes against the synced database. The common mistake is building and maintaining a whole custom integration just to power dashboards that never needed sub-minute freshness, then paying for that decision in token refreshes, 429 retries, and reconciliation jobs for years.
Getting Started with Database Sync
If you'd rather not build and babysit all of that, Codeless Sync connects your PostgreSQL database and syncs Xero data, contacts, invoices, payments, accounts, bank transactions, credit notes, items, purchase orders, journals, and organisation details, in about 5 minutes. It handles the OAuth consent and token refresh for you, lets you pick which organisation to sync, auto-creates the destination tables, and upserts on every run so there are no duplicates. There's a free tier, no credit card required.
The same model works for Stripe, QuickBooks, and Paddle too, so if your finances span more than one provider, all of it lands in the same Postgres database. You can point it at Supabase, Neon, Railway, or AWS RDS, whichever host you already use.
Frequently Asked Questions
Should I build my own Xero integration or use a sync tool?
Build it if you need to write data back into Xero, want real-time reaction to specific events, or already run pipeline infrastructure. Use a sync tool if you mainly need a queryable copy of your accounting data for dashboards, reporting, or reconciliation, and you would rather not own OAuth token rotation, tenant routing, and rate-limit handling. Plenty of teams do both: the API for the few real-time events, managed sync for everything else.
What does it actually cost to maintain a Xero API integration?
The code is a one-time build. The cost is everything after: rotating the single-use refresh token on every refresh before the 60-day window lapses, backing off when you hit the 60-calls-per-minute or 5-concurrent limit on each organisation, running a periodic reconciliation for changes that don't bump UpdatedDateUTC, and updating your mapping when Xero changes a payload. That is recurring engineer time, which is what a flat-rate managed sync is really replacing.
Does a managed Xero sync still use the Xero API?
Yes. A sync tool like Codeless Sync talks to the same OAuth 2.0 and Accounting API endpoints under the hood. The difference is that it operates the consent flow, the tenant selection, the pagination, and the schema mapping for you, so what you end up with is a set of PostgreSQL tables to query rather than a pipeline to maintain.
Can Xero webhooks replace a scheduled sync?
Not for analytics. Xero webhooks only fire for contacts, invoices, credit notes, and App Store subscriptions, emit create and update events only (no deletes), and carry just a reference rather than the record itself, so you still call the API to fetch the data. For a complete, queryable copy of your accounting data, a scheduled sync is more reliable and needs no public endpoint. Use webhooks only for the specific events you must react to the instant they happen.
Which Xero data can I sync to my database?
Codeless Sync supports ten Xero data types: contacts, invoices, payments, accounts, bank transactions, credit notes, items, purchase orders, journals, and organisation details, each written to its own PostgreSQL table. After the first full pull, later runs are incremental, so each sync only brings over what changed.
Related:
Questions or feedback? Feel free to reach out. If you found this helpful, you can try Codeless Sync for free.