Xero API Patterns
Overview
The Xero API is a RESTful JSON API that provides access to accounting data including contacts, invoices, payments, accounts, bank transactions, credit notes, and reports. This skill covers OAuth2 authentication (Custom Connections), query building, pagination, error handling, and performance optimization patterns specific to MSP billing operations.
Authentication
OAuth2 Custom Connections
Xero uses OAuth2 with Custom Connections for machine-to-machine (M2M) integrations. This is the recommended approach for server-side automations and CLI tools.
Token Request:
POST https://identity.xero.com/connect/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
grant_type=client_credentials&scope=accounting.transactions accounting.contacts accounting.reports.read accounting.settings
curl Example:
curl -s -X POST https://identity.xero.com/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "${XERO_CLIENT_ID}:${XERO_CLIENT_SECRET}" \
-d "grant_type=client_credentials&scope=accounting.transactions accounting.contacts accounting.reports.read accounting.settings"
Token Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI...",
"expires_in": 1800,
"token_type": "Bearer",
"scope": "accounting.transactions accounting.contacts accounting.reports.read accounting.settings"
}
Token Details:
| Field | Value | Description |
|---|---|---|
access_token | JWT string | Bearer token for API requests |
expires_in | 1800 | Token lifetime in seconds (30 minutes) |
token_type | Bearer | Token type for Authorization header |
scope | Space-delimited | Granted OAuth scopes |
Required Headers
All API requests require these headers:
| Header | Value | Description |
|---|---|---|
Authorization | Bearer {access_token} | OAuth2 access token |
xero-tenant-id | Your tenant ID | Target Xero organization |
Content-Type | application/json | JSON content type |
Accept | application/json | JSON response format |
Environment Variables
export XERO_CLIENT_ID="your-client-id"
export XERO_CLIENT_SECRET="your-client-secret"
export XERO_TENANT_ID="your-tenant-id"
Base URL Pattern
All accounting API endpoints follow the pattern:
https://api.xero.com/api.xro/2.0/{resource}
Examples:
https://api.xero.com/api.xro/2.0/Contacts
https://api.xero.com/api.xro/2.0/Invoices
https://api.xero.com/api.xro/2.0/Payments
https://api.xero.com/api.xro/2.0/Accounts
OAuth Scopes
| Scope | Description |
|---|---|
accounting.transactions | Read/write invoices, payments, credit notes, bank transactions |
accounting.transactions.read | Read-only access to transactions |
accounting.contacts | Read/write contacts |
accounting.contacts.read | Read-only access to contacts |
accounting.reports.read | Read financial reports |
accounting.settings | Read/write chart of accounts, tax rates |
accounting.settings.read | Read-only access to settings |
Token Management Pattern
class XeroAuth {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = null;
this.expiresAt = 0;
}
async getToken() {
if (this.accessToken && Date.now() < this.expiresAt - 60000) {
return this.accessToken;
}
const credentials = Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64');
const response = await fetch('https://identity.xero.com/connect/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`
},
body: 'grant_type=client_credentials&scope=accounting.transactions accounting.contacts accounting.reports.read accounting.settings'
});
const data = await response.json();
this.accessToken = data.access_token;
this.expiresAt = Date.now() + (data.expires_in * 1000);
return this.accessToken;
}
}
Request Format
Standard API Request
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
POST Request (Create)
curl -s -X POST "https://api.xero.com/api.xro/2.0/Invoices" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"Type": "ACCREC",
"Contact": { "ContactID": "abc-123" },
"LineItems": [
{
"Description": "Monthly Managed Services",
"Quantity": 1,
"UnitAmount": 2500.00,
"AccountCode": "200"
}
],
"Date": "2026-03-01T00:00:00",
"DueDate": "2026-03-31T00:00:00"
}'
Response Format
Single Resource:
{
"Id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"Status": "OK",
"Contacts": [
{
"ContactID": "abc-123",
"Name": "Acme Corp",
"EmailAddress": "billing@acme.com",
"ContactStatus": "ACTIVE"
}
]
}
Collection:
{
"Id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"Status": "OK",
"Invoices": [
{
"InvoiceID": "inv-456",
"Type": "ACCREC",
"InvoiceNumber": "INV-0042",
"Contact": { "ContactID": "abc-123", "Name": "Acme Corp" },
"Total": 2500.00,
"Status": "AUTHORISED"
}
]
}
Filtering
Where Clause Filtering
Xero uses a where query parameter with OData-style expressions:
GET /api.xro/2.0/Contacts?where=Name=="Acme Corp"
GET /api.xro/2.0/Contacts?where=Name.StartsWith("Acme")
GET /api.xro/2.0/Contacts?where=ContactStatus=="ACTIVE"
GET /api.xro/2.0/Invoices?where=Type=="ACCREC"&&Status=="AUTHORISED"
GET /api.xro/2.0/Invoices?where=Contact.ContactID==guid("abc-123")
GET /api.xro/2.0/Invoices?where=AmountDue>0
Important: URL-encode the where parameter value in actual requests.
Where Clause Operators
| Operator | Description | Example |
|---|---|---|
== | Equals | Name=="Acme" |
!= | Not equals | Status!="DELETED" |
> | Greater than | AmountDue>0 |
< | Less than | AmountDue<100 |
>= | Greater or equal | Total>=1000 |
<= | Less or equal | Total<=5000 |
&& | AND | Type=="ACCREC"&&Status=="PAID" |
| ` | ` | |
.StartsWith() | Starts with | Name.StartsWith("Acme") |
.EndsWith() | Ends with | Name.EndsWith("Corp") |
.Contains() | Contains | Name.Contains("tech") |
If-Modified-Since Header
Use this header to retrieve only records modified after a given date:
GET /api.xro/2.0/Contacts
If-Modified-Since: 2026-02-01T00:00:00
Order Parameter
Sort results using the order parameter:
GET /api.xro/2.0/Invoices?order=Date DESC
GET /api.xro/2.0/Contacts?order=Name ASC
Pagination
Page-Based Pagination
Xero uses page-based pagination with the page query parameter:
GET /api.xro/2.0/Invoices?page=1
GET /api.xro/2.0/Invoices?page=2
Pagination Details:
| Parameter | Description | Default |
|---|---|---|
page | Page number (1-based) | 1 |
| Results per page | Fixed by Xero | 100 |
Detecting End of Pages
When a page returns fewer than 100 results, you have reached the last page:
async function fetchAllInvoices(auth, tenantId) {
const allItems = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const token = await auth.getToken();
const response = await fetch(
`https://api.xero.com/api.xro/2.0/Invoices?page=${page}`,
{
headers: {
'Authorization': `Bearer ${tok