From xero
Provides patterns for Xero API including OAuth2 Custom Connections auth, tenant headers, REST structure, filtering, pagination, rate limiting, error handling, and batch operations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/xero:api-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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 |
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 |
export XERO_CLIENT_ID="your-client-id"
export XERO_CLIENT_SECRET="your-client-secret"
export XERO_TENANT_ID="your-tenant-id"
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
| 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 |
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;
}
}
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"
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"
}'
Single Resource:
{
"Id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"Status": "OK",
"Contacts": [
{
"ContactID": "abc-123",
"Name": "Acme Corp",
"EmailAddress": "[email protected]",
"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"
}
]
}
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.
| 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") |
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
Sort results using the order parameter:
GET /api.xro/2.0/Invoices?order=Date DESC
GET /api.xro/2.0/Contacts?order=Name ASC
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 |
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 ${token}`,
'xero-tenant-id': tenantId,
'Accept': 'application/json'
}
}
);
const data = await response.json();
const invoices = data.Invoices || [];
allItems.push(...invoices);
hasMore = invoices.length === 100;
page++;
}
return allItems;
}
These endpoints require pagination (return max 100 per page):
| Endpoint | Paginated | Notes |
|---|---|---|
/Contacts | Yes | 100 per page |
/Invoices | Yes | 100 per page |
/Payments | Yes | 100 per page |
/BankTransactions | Yes | 100 per page |
/CreditNotes | Yes | 100 per page |
/Accounts | No | Returns all accounts |
/Reports/* | No | Returns full report |
| Metric | Limit |
|---|---|
| Requests per minute | 60 |
| Requests per day | 5,000 |
Xero returns rate limit information in response headers:
| Header | Description |
|---|---|
X-Rate-Limit-Problem | Present when rate limited |
Retry-After | Seconds to wait before retry |
When rate limited (HTTP 429):
{
"Type": "RateLimitException",
"Message": "Rate limit exceeded. Please wait before making more requests.",
"Detail": "Minute rate limit exceeded"
}
async function requestWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
const jitter = Math.random() * 5000;
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await new Promise(r => setTimeout(r, retryAfter * 1000 + jitter));
continue;
}
if (response.status >= 500) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Server error ${response.status}. Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
continue;
}
return response;
}
throw new Error(`Max retries (${maxRetries}) exceeded`);
}
Xero uses the format YYYY-MM-DDT00:00:00 for dates:
{
"Date": "2026-03-01T00:00:00",
"DueDate": "2026-03-31T00:00:00"
}
Some responses return dates in Microsoft JSON format:
{
"Date": "/Date(1772524800000+0000)/"
}
Parse this by extracting the timestamp:
function parseXeroDate(dateString) {
if (dateString.startsWith('/Date(')) {
const timestamp = parseInt(dateString.match(/\d+/)[0], 10);
return new Date(timestamp);
}
return new Date(dateString);
}
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 400 | Bad Request | Check request format and required fields |
| 401 | Unauthorized | Refresh access token |
| 403 | Forbidden | Check tenant ID and scopes |
| 404 | Not Found | Resource doesn't exist |
| 429 | Rate Limited | Wait and retry with backoff |
| 500 | Server Error | Retry with backoff |
Xero returns validation errors as an array within the resource:
{
"Id": "...",
"Status": "OK",
"Invoices": [
{
"InvoiceID": "00000000-0000-0000-0000-000000000000",
"HasErrors": true,
"ValidationErrors": [
{
"Message": "Account code '999' is not a valid code for this document."
},
{
"Message": "A Contact is required to create an Invoice."
}
]
}
]
}
function handleXeroResponse(data, resourceName) {
const resources = data[resourceName] || [];
for (const resource of resources) {
if (resource.HasErrors && resource.ValidationErrors) {
const errors = resource.ValidationErrors.map(e => e.Message);
throw new Error(`Validation errors: ${errors.join('; ')}`);
}
}
return resources;
}
Xero supports sending multiple resources in a single request:
curl -s -X POST "https://api.xero.com/api.xro/2.0/Invoices?summarizeErrors=false" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Content-Type: application/json" \
-d '{
"Invoices": [
{
"Type": "ACCREC",
"Contact": { "ContactID": "abc-123" },
"LineItems": [{ "Description": "Managed Services - March", "Quantity": 1, "UnitAmount": 2500.00, "AccountCode": "200" }],
"Date": "2026-03-01T00:00:00",
"DueDate": "2026-03-31T00:00:00"
},
{
"Type": "ACCREC",
"Contact": { "ContactID": "def-456" },
"LineItems": [{ "Description": "Managed Services - March", "Quantity": 1, "UnitAmount": 1800.00, "AccountCode": "200" }],
"Date": "2026-03-01T00:00:00",
"DueDate": "2026-03-31T00:00:00"
}
]
}'
Use ?summarizeErrors=false to get per-item error details in batch operations.
where parameter must be properly encoded| Endpoint | Methods | Description |
|---|---|---|
/Contacts | GET, POST, PUT | Customer and supplier contacts |
/Invoices | GET, POST, PUT | Sales invoices and bills |
/Payments | GET, POST, PUT, DELETE | Payment records |
/Accounts | GET, POST, PUT, DELETE | Chart of accounts |
/CreditNotes | GET, POST, PUT | Credit notes |
/BankTransactions | GET, POST, PUT | Bank transactions |
/Reports/ProfitAndLoss | GET | Profit and Loss report |
/Reports/BalanceSheet | GET | Balance Sheet report |
/Reports/AgedReceivablesByContact | GET | Aged Receivables report |
/Reports/AgedPayablesByContact | GET | Aged Payables report |
/Reports/TrialBalance | GET | Trial Balance report |
/TaxRates | GET | Tax rate configurations |
/Currencies | GET | Configured currencies |
npx claudepluginhub p/drewrox2009-xero-msp-claude-plugins-xero-xeroAutomates Xero accounting operations: manage invoices, contacts, payments, bank transactions, and chart of accounts for small business bookkeeping.
Provides QuickBooks Online API patterns for OAuth2 authentication, REST endpoints, Intuit query language, pagination, rate limiting, error handling, minor versions, base URLs, and sandbox/production setups.
Reconciles Pax8 cloud subscriptions against Xero or QuickBooks Online invoices to identify billing gaps, unbilled subscriptions, quantity mismatches, and margin discrepancies.