Skip to content

How to Integrate with the Xero API: A Guide for B2B SaaS

A technical guide to building a production-grade Xero API integration, covering OAuth 2.0 token management, rate limits, webhook validation, and the 2026 pricing shift.

Roopendra Talekar Roopendra Talekar · · 13 min read
How to Integrate with the Xero API: A Guide for B2B SaaS

Integrating with the Xero API means handling OAuth 2.0 tokens that expire every 30 minutes, respecting a hard cap of 60 API calls per minute per organization, implementing HMAC-SHA256 webhook validation with a 5-second response window, and — as of March 2026 — planning for Xero's new usage-based pricing model that charges per gigabyte of data egress.

If your product managers and customers are asking "how do I integrate with the Xero API?", understand that you are staring down a significant engineering initiative. Xero is a dominant force in the global accounting software market, especially in the UK, Australia, and New Zealand, with an ecosystem of over 500 apps serving 4.6 million global subscribers. For any B2B SaaS application touching financial data — whether you are building an expense management tool, an AI-driven invoice processor, or a specialized CRM — a Xero integration is a non-negotiable requirement.

But building a reliable Xero API integration is deceptively complex. It is not a matter of mapping JSON payloads to a REST endpoint. Xero enforces architectural constraints that will quickly break naive implementations. This guide breaks down the technical requirements, limitations, and architectural best practices for building a production-grade Xero integration — and helps you evaluate whether your team should build this in-house or leverage a unified accounting API.

For more context on why financial connectivity is expanding across software categories, read our guide on Why B2B Fintech Needs More Than Bank Data: Embracing Unified APIs for Core Business Systems.

Xero OAuth 2.0 Authentication: The 30-Minute Token Problem

Xero uses the standard OAuth 2.0 Authorization Code flow. The API does not support basic API key authentication — you must register your app in the Xero Developer Portal to get a client ID and client secret. When a user connects their Xero tenant to your application, you receive an access token and a refresh token.

Here is the architectural trap: Xero access tokens expire after exactly 30 minutes. This is an exceptionally short lifespan compared to other SaaS platforms. Worse, if a refresh token sits unused for 60 days, it expires silently. That means if a customer connects Xero and then does not trigger any syncs for two months, the connection dies without warning.

If your application relies on background workers to sync invoices, reconcile payments, or pull ledger data, those workers will constantly encounter expired tokens. You cannot rely on a user being present in the browser to re-authenticate. You must build an automated, highly resilient refresh token architecture.

The Race Condition Threat

The 30-minute window creates a specific and dangerous architectural challenge. If your app has multiple workers or background jobs hitting the Xero API concurrently, two processes might detect the expired token at the same time and both attempt a refresh.

Imagine Worker A and Worker B both attempt to sync data for the same Xero tenant at minute 31. Both realize the access token is expired. Both attempt to use the same refresh token simultaneously. Xero's authorization server processes Worker A's request, issues a new token pair, and invalidates the old refresh token. Milliseconds later, Worker B's request arrives using the now-invalidated refresh token. Xero detects this as potential token reuse and revokes the entire token family. Your integration drops the connection with an invalid_grant error, and the customer is forced to manually re-authenticate.

This is not a theoretical risk. One developer in the Xero community forum described "risking a race condition with every Xero account every 30 minutes" and "getting locked out of Xero about twice a day."

To handle this correctly, you need:

  1. A mutex or lock around the refresh operation — only one process should refresh a given account's token at a time
  2. Proactive refresh before expiry — do not wait for the token to actually expire; refresh 60–120 seconds ahead
  3. Retry with re-read — if a refresh fails with invalid_grant, re-read the stored token (another process may have already refreshed it successfully)
  4. Alerting on refresh failures — when a refresh ultimately fails, mark the account and notify the customer to re-authenticate
sequenceDiagram
    participant Worker A
    participant Worker B
    participant Lock as Token Lock
    participant Store as Token Store
    participant Xero as Xero OAuth

    Worker A->>Lock: Acquire lock (account_id)
    Lock-->>Worker A: Lock acquired
    Worker B->>Lock: Acquire lock (account_id)
    Lock-->>Worker B: Wait (lock busy)
    Worker A->>Store: Read current token
    Store-->>Worker A: Token (expires in 45s)
    Worker A->>Xero: POST /token (refresh_token)
    Xero-->>Worker A: New access_token + refresh_token
    Worker A->>Store: Save new tokens
    Worker A->>Lock: Release lock
    Lock-->>Worker B: Lock released
    Worker B->>Store: Read new tokens (no API call needed)

Do not attempt to write a naive setInterval script for token refreshes. You need a centralized locking mechanism to ensure only one process can refresh a specific tenant's token at a time.

For a deeper dive into this architectural pattern, see our guide on OAuth at Scale: The Architecture of Reliable Token Refreshes.

Xero's API rate limits are strict. If you treat Xero like an internal database, your application will quickly grind to a halt. Xero enforces four separate rate limit axes that interact in non-obvious ways:

  • 5 concurrent requests in progress at a time
  • 60 API calls per minute per organization
  • 5,000 API calls per day per organization
  • 10,000 calls per minute across all tenancies for a single app (app-wide limit)

These limits are per-organization, per-app. If two separate Xero organizations are connected to your application, each connection has its own 5,000 daily call budget. Each API response includes X-DayLimit-Remaining, X-MinLimit-Remaining, and X-AppMinLimit-Remaining headers, so you can track consumption in real time.

The Math Problem

5,000 daily calls sounds generous until you do the math. Syncing a single invoice with line items, contacts, and payment records can consume up to 6 API calls. If a customer has 1,000 invoices, a full initial sync could blow through your daily limit before you even touch contacts or payments.

The Concurrency Trap

The 5 concurrent request limit is often the first bottleneck engineering teams hit. If you write a Node.js script that uses Promise.all() to fetch 10 resources simultaneously, Xero will immediately reject 5 of those requests. You must throttle your outbound HTTP requests at the application level.

Handling 429 Too Many Requests

When you exceed these limits, Xero returns a 429 Too Many Requests HTTP status code with a Retry-After header that tells you how many seconds to wait. Your integration must catch this error, respect the header, and retry intelligently.

The standard approach is exponential backoff with jitter:

import pRetry from 'p-retry';
import pLimit from 'p-limit';
 
// Restrict concurrency to 4 to stay safely under Xero's limit of 5
const limit = pLimit(4);
 
async function fetchXeroResource(url: string, accessToken: string) {
  const runFetch = async () => {
    const response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Accept': 'application/json'
      }
    });
 
    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      throw new Error('Rate limited by Xero');
    }
 
    if (!response.ok) {
      throw new pRetry.AbortError(`Failed with status: ${response.status}`);
    }
 
    return response.json();
  };
 
  return limit(() => pRetry(runFetch, {
    retries: 5,
    minTimeout: 2000,
    maxTimeout: 30000,
    randomize: true // Adds jitter to prevent thundering herd
  }));
}

But backoff is the wrong place to spend all your architectural effort. The real strategy is to minimize calls in the first place:

  • Use If-Modified-Since headers for incremental syncs — Xero supports this header to retrieve only records changed since your previous request
  • Batch writes — you can create multiple invoices in a single PUT or POST call, with Xero recommending up to 50 elements per request within a 3.5 MB size limit
  • Increase page size — Xero now allows up to 1,000 results per page (up from the old default of 100), meaning 10,000 records take 10 calls instead of 100
  • Use webhooks for change detection instead of polling endpoints

For broader strategies on managing API quotas across multiple providers, review Best Practices for Handling API Rate Limits and Retries Across Multiple Third-Party APIs.

Working with Xero API Webhooks and Pagination

Reading historical data requires pagination. Reacting to new data requires webhooks. Both have specific quirks in the Xero ecosystem.

The Intent to Receive (ITR) Challenge

Xero webhooks are not fire-and-forget. Before Xero sends live event data to your endpoint, you must pass a strict validation process known as Intent to Receive (ITR).

When you register your webhook URL in the Xero developer portal, Xero immediately sends a POST request to your endpoint with an x-xero-signature header. Your server has exactly 5 seconds to:

  1. Read the payload (which may be empty for the ITR check)
  2. Hash the payload using HMAC-SHA256, signed with your unique Xero Webhook Key
  3. Base64 encode the resulting hash
  4. Compare your generated hash against the x-xero-signature header using a timing-safe comparison
  5. Respond with 200 OK for valid signatures, or 401 Unauthorized for invalid ones

If your server takes 6 seconds, or if you respond with a 201 Created instead of 200 OK, the validation fails. Xero will disable your webhook. Furthermore, every subsequent live event payload must be validated using this exact same cryptographic process. If your endpoint repeatedly fails to validate signatures or returns errors, Xero will penalize the endpoint and eventually stop sending events.

The gotcha that trips up most teams: frameworks that automatically parse JSON break signature validation, since HMAC calculation requires the raw request body. You must access unparsed request data.

Here is a correct Node.js implementation:

import crypto from 'crypto';
import express from 'express';
 
const app = express();
 
// Critical: capture raw body BEFORE JSON parsing
app.post('/xero-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-xero-signature'] as string;
  const rawBody = req.body; // Buffer, not parsed JSON
 
  const hmac = crypto.createHmac('sha256', process.env.XERO_WEBHOOK_KEY!);
  const computed = hmac.update(rawBody).digest('base64');
 
  // Use timing-safe comparison to prevent timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
 
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
 
  // Acknowledge immediately, process async
  res.status(200).send();
 
  const payload = JSON.parse(rawBody.toString());
  processEventsAsync(payload.events); // Queue for background processing
});
Warning

Do not process events synchronously inside the webhook handler. Servers under high load that exceed Xero's 5-second response requirement trigger retry loops. Xero's automatic retry logic can overwhelm already-stressed servers, creating exponentially increasing webhook queues that effectively break the integration until manually reset. Always acknowledge first, then process in a background queue. For more on this architectural pattern, see our guide on how mid-market SaaS teams handle API rate limits and webhooks at scale.

Handling Pagination for Data Syncs

Xero uses page-based pagination with a page query parameter (1-indexed) and a pageSize parameter. The default page size is 100, with a maximum of 1,000 and a minimum of 1. If you set a value outside the supported range, Xero automatically adjusts it to the nearest valid page size. You know you have reached the end when the response returns fewer items than the page size.

GET /api.xro/2.0/Invoices?page=1&pageSize=1000

Pagination is supported on Invoices, Contacts, Bank Transactions, Credit Notes, Payments, Manual Journals, Quotes, and Purchase Orders.

Even at the maximum page size, an enterprise customer with 50,000 invoices requires 50 sequential API calls. Combined with the 60 calls per minute rate limit, a full sync of just invoices can take several minutes. Your system architecture must handle long-running background jobs without timing out HTTP requests.

Do not attempt to load all paginated results into memory before saving them to your database. Use async generators or stream processing to transform and persist each page before requesting the next one. This keeps your application's memory footprint flat regardless of the tenant's data volume.

The March 2026 Pricing Shift: What B2B SaaS Teams Need to Know

This is a recent and significant change that affects the economics of any Xero integration. Starting March 2, 2026, Xero retired its revenue share model in favor of usage-based pricing. Developers are now charged based on two axes: the number of connections (how many Xero customers use your app) and data egress (how much data you pull out of Xero).

The new model includes five tiers — Starter, Core, Plus, Advanced, and Enterprise. Monthly fees range from $0 for Starter to $895 for Advanced, with Enterprise pricing negotiated individually. The Starter tier is also capped at 1,000 API calls per day, down from the standard 5,000.

There is also a notable policy update: Xero now prohibits the use of data obtained through its APIs to train or contribute to any AI or machine learning model.

For B2B SaaS teams, this creates a direct incentive to minimize data egress. If your app needs to download large volumes of transaction data for reporting or analytics, you get billed for that data usage. The practical implication: cache aggressively, use webhooks instead of polling, query only the fields you need, and seriously evaluate whether a synced data layer can reduce your direct API consumption.

Build vs. Buy: The True Cost of a Native Xero Integration

Let's put real numbers on this. Building a single, production-ready accounting integration in-house typically requires around 30 person-days of engineering time. Depending on team location and seniority, that translates to roughly $20,000–$40,000 per connector in direct engineering cost. This includes:

  • Designing the OAuth 2.0 flow and distributed token refresh locks
  • Building the queuing system for rate limit management and exponential backoff
  • Setting up cryptographic validation for webhooks
  • Mapping Xero's specific data models (Invoices, Contacts, Accounts) to your internal schema
  • Writing integration tests against Xero's sandbox environment

Once deployed, the hidden cost of maintenance begins. Expect roughly 40 person-days annually covering API version deprecations, Xero's new granular scopes rollout (coming April 2026), pagination changes, rate limit adjustments, edge cases in the data model (multi-currency invoices, tax rate overrides), and compliance with the new pricing model.

Here is the real question: is Xero the only accounting integration your customers will ever need?

If you are selling to mid-market and enterprise, the answer is almost certainly no. You will also need QuickBooks Online, NetSuite, and potentially Sage or Zoho Books. Each one has its own authentication flow, field naming conventions, rate limits, and edge cases. What was a 30-day project for Xero becomes a 6-month integration marathon across four platforms — scaling linearly in cost and exponentially in technical debt.

For a detailed breakdown of this calculation, see Build vs. Buy: The True Cost of Building SaaS Integrations In-House.

How Truto Simplifies Xero Integrations for B2B SaaS

Instead of dedicating your engineering team to building and maintaining Xero-specific infrastructure, modern B2B SaaS companies use unified APIs to abstract away the complexity.

Truto's Unified Accounting API normalizes Xero's endpoints into a standardized schema that also works for QuickBooks Online, NetSuite, Zoho Books, and others. You integrate once against a common data model covering Invoices, Contacts, Payments, Expenses, Journal Entries, and the rest of the accounting lifecycle — and every provider works through the same interface.

Here is what that looks like in practice:

# List invoices from Xero - same call works for QuickBooks, NetSuite, etc.
curl -X GET "https://api.truto.one/unified/accounting/invoices?integrated_account_id=xero_abc123&limit=50" \
  -H "Authorization: Bearer YOUR_TRUTO_API_KEY"

The response follows a unified schema regardless of the underlying provider:

{
  "result": [
    {
      "id": "inv_001",
      "contact": { "id": "c_42", "name": "Acme Corp" },
      "status": "AUTHORISED",
      "total": 1500.00,
      "currency": "NZD",
      "due_date": "2026-04-15",
      "line_items": ["..."],
      "remote_data": { }
    }
  ],
  "next_cursor": "..."
}

The remote_data field preserves the full original Xero response, giving you escape-hatch access to provider-specific fields that do not map to the unified schema.

Beyond schema normalization, Truto handles the operational headaches discussed throughout this guide:

  • Proactive Token Management: Truto does not wait for tokens to fail. The platform schedules background refresh of Xero access tokens 60 to 180 seconds before they expire, using mutex-locked concurrency control to guarantee race conditions never occur. If a refresh ultimately fails, the account is immediately flagged as needs_reauth and a webhook event fires to your app so you can prompt the customer.
  • Rate Limit Awareness: The platform respects Xero's Retry-After headers and backs off automatically. For read-heavy use cases like dashboards, reporting, or analytics, Truto can sync Xero data into a queryable store so your application reads from local data instead of hitting Xero's rate limits directly. This is particularly valuable under the new egress-based pricing.
  • Automated Webhook Normalization: Truto handles Xero's Intent to Receive validation and HMAC-SHA256 signature verification automatically. When Xero fires a raw webhook, Truto catches it, verifies it, fetches the full updated record, transforms it into a standardized schema, and delivers a clean record:updated event to your application.
  • Standardized Schema: Xero calls them "Contacts". QuickBooks calls them "Customers" and "Vendors". Truto normalizes these entities into a single, predictable schema. You write code once against the Truto API and it works across every accounting platform.
Info

Honest trade-off: A unified API adds a layer of abstraction. If you need deep, Xero-specific functionality that does not map to common accounting concepts — like Xero Practice Manager data, which is now gated behind the Advanced tier at $895/month — the unified layer will not cover it. For those cases, Truto provides a Proxy API that lets you make raw, unmapped calls directly to Xero through the same authenticated connection.

What This Means for Your Integration Roadmap

If Xero is your only accounting integration and you have dedicated engineering bandwidth, building natively is defensible. Budget 30 days for the initial build, plan for the OAuth race condition problem from day one, and factor in Xero's new pricing model when estimating data sync costs.

If your customers also use QuickBooks, NetSuite, or Sage — or if they will in the next 12 months — building each connector from scratch will burn through engineering time that should be spent on your core product. A unified API collapses the N-connector problem into a single integration.

The math is straightforward. Four native connectors: roughly 120 person-days of build time, plus roughly 160 person-days of annual maintenance. One unified API integration: days, not months — with the provider handling token refresh, rate limits, schema normalization, and webhook plumbing across all four platforms.

Whichever path you choose, start with the hard parts first: get the OAuth token lifecycle right, respect the rate limits, and never process webhooks synchronously. Everything else is field mapping.

FAQ

What are the Xero API rate limits?
Xero enforces 60 calls per minute, 5,000 calls per day, and a maximum of 5 concurrent requests per organization per app. There is also an app-wide limit of 10,000 calls per minute across all tenants. Exceeding any limit returns an HTTP 429 response with a Retry-After header.
How long do Xero OAuth 2.0 access tokens last?
Xero access tokens expire after 30 minutes. Refresh tokens expire after 60 days of inactivity. You need to implement proactive token refresh with concurrency control to avoid race conditions that can lock out your integration.
How do Xero webhooks work?
Xero webhooks require HMAC-SHA256 signature validation using the x-xero-signature header. Your endpoint must respond within 5 seconds with HTTP 200 for valid requests and HTTP 401 for invalid signatures. You must also pass an initial Intent to Receive validation before receiving real events.
How much does it cost to build a Xero API integration?
A production-grade Xero integration typically takes around 30 person-days to build, translating to $20,000-$40,000 in direct engineering cost. Annual maintenance adds roughly 40 person-days on top of that, covering API changes, token management, and rate limit adjustments.
What is Xero's new developer pricing model for 2026?
Starting March 2, 2026, Xero charges developers based on connection count and data egress volume across five tiers: Starter ($0), Core, Plus, Advanced ($895/month), and Enterprise (negotiated). The Starter tier is limited to 1,000 API calls per day instead of the standard 5,000.

More from our Blog