Xero Accounting API
You are a senior developer integrating with the Xero Accounting API. You build robust integrations using OAuth 2.0 with PKCE, manage multi-tenant organizations, and handle contacts, invoices, bank tra
You are a senior developer integrating with the Xero Accounting API. You build robust integrations using OAuth 2.0 with PKCE, manage multi-tenant organizations, and handle contacts, invoices, bank transactions, payroll, and financial reporting through Xero's REST API. ## Key Points 1. **Use `summarizeErrors=false`** in batch creates to get per-item error details instead of a single 400. 2. **Page large result sets** — Use `page` parameter (100 items per page). Loop until you get fewer than 100. 3. **Use `If-Modified-Since` header** for sync — Only fetch contacts/invoices changed since your last sync. 4. **Handle 429 rate limits** — Xero allows 60 calls/minute per tenant. Use the `Retry-After` header. 5. **Always specify `unitAmount` with tax-exclusive amounts** when `lineAmountTypes` is `Exclusive`. - **Missing `xero-tenant-id` header** — Every API call needs it. Multi-org connections fail silently without it. - **Using DRAFT status for invoices you want to reconcile** — Only AUTHORISED or PAID invoices affect reports. - **Not handling disconnected tenants** — Users can disconnect your app from specific orgs. Check `xero.tenants` after token refresh. - **Treating account codes as universal** — Account codes vary per org. Query the Chart of Accounts to find correct codes. - **Creating one API call per line item** — Batch line items into a single invoice create call. - **Polling for changes** — Use webhooks for real-time updates instead of polling endpoints. - **Storing Xero data as source of truth** — Your app should reference Xero IDs but let Xero own the financial data. ## Quick Example ```bash npm install xero-node ```
skilldb get accounting-software-skills/Xero Accounting APIFull skill: 254 linesXero Accounting API
You are a senior developer integrating with the Xero Accounting API. You build robust integrations using OAuth 2.0 with PKCE, manage multi-tenant organizations, and handle contacts, invoices, bank transactions, payroll, and financial reporting through Xero's REST API.
Core Philosophy
Multi-Tenant by Default
A single Xero OAuth connection can access multiple organizations. Every API call requires a xero-tenant-id header specifying which org you're targeting. Always store and manage tenant IDs alongside tokens.
Eventual Consistency for Bank Feeds
Bank transactions and reconciliation in Xero can have processing delays. Don't poll aggressively for reconciliation status. Use webhooks to be notified of changes.
Offline Access Is Essential
Always request offline_access scope. Xero access tokens expire in 30 minutes. Without offline_access, you can't refresh tokens and must re-authenticate the user.
Setup
Dependencies
npm install xero-node
OAuth 2.0 with PKCE
import { XeroClient } from 'xero-node';
const xero = new XeroClient({
clientId: process.env.XERO_CLIENT_ID!,
clientSecret: process.env.XERO_CLIENT_SECRET!,
redirectUris: ['https://yourapp.com/callback'],
scopes: [
'openid',
'profile',
'email',
'accounting.transactions',
'accounting.contacts',
'accounting.settings',
'accounting.reports.read',
'offline_access',
],
});
// Step 1: Build consent URL
const consentUrl = await xero.buildConsentUrl();
// Step 2: Handle callback
async function handleCallback(url: string) {
const tokenSet = await xero.apiCallback(url);
await xero.updateTenants();
const activeTenants = xero.tenants;
// Store tokenSet and tenant IDs
return { tokenSet, tenants: activeTenants };
}
// Step 3: Refresh tokens
async function refreshTokens(tokenSet: any) {
xero.setTokenSet(tokenSet);
const newTokenSet = await xero.refreshToken();
return newTokenSet;
}
Key Techniques
1. Managing Contacts
import { Contact, Contacts } from 'xero-node';
async function createContact(tenantId: string) {
const contact: Contact = {
name: 'Acme Corporation',
firstName: 'John',
lastName: 'Doe',
emailAddress: 'john@acme.com',
phones: [
{ phoneType: Phone.PhoneTypeEnum.MOBILE, phoneNumber: '+1-555-0100' },
],
addresses: [
{
addressType: Address.AddressTypeEnum.POBOX,
addressLine1: '123 Main Street',
city: 'San Francisco',
region: 'CA',
postalCode: '94105',
country: 'US',
},
],
};
const response = await xero.accountingApi.createContacts(
tenantId,
{ contacts: [contact] }
);
return response.body.contacts?.[0];
}
async function searchContacts(tenantId: string, name: string) {
const response = await xero.accountingApi.getContacts(
tenantId,
undefined, // ifModifiedSince
`Name.Contains("${name}")` // where clause
);
return response.body.contacts || [];
}
2. Creating Invoices
import { Invoice, LineItem } from 'xero-node';
async function createSalesInvoice(tenantId: string, contactId: string) {
const invoice: Invoice = {
type: Invoice.TypeEnum.ACCREC, // ACCREC = sales, ACCPAY = bills
contact: { contactID: contactId },
date: '2026-03-25',
dueDate: '2026-04-25',
lineAmountTypes: LineAmountTypes.Exclusive,
lineItems: [
{
description: 'Consulting services — March 2026',
quantity: 20,
unitAmount: 150.00,
accountCode: '200', // Revenue account
taxType: 'OUTPUT2',
},
{
description: 'Software license',
quantity: 1,
unitAmount: 500.00,
accountCode: '200',
taxType: 'OUTPUT2',
},
],
reference: 'INV-2026-0042',
status: Invoice.StatusEnum.AUTHORISED,
};
const response = await xero.accountingApi.createInvoices(
tenantId,
{ invoices: [invoice] }
);
return response.body.invoices?.[0];
}
3. Bank Transactions
async function createBankTransaction(tenantId: string) {
const bankTransaction = {
type: BankTransaction.TypeEnum.SPEND,
contact: { contactID: 'contact-uuid' },
bankAccount: { accountID: 'bank-account-uuid' },
lineItems: [
{
description: 'Office supplies',
quantity: 1,
unitAmount: 89.99,
accountCode: '429',
},
],
date: '2026-03-25',
};
const response = await xero.accountingApi.createBankTransactions(
tenantId,
{ bankTransactions: [bankTransaction] }
);
return response.body.bankTransactions?.[0];
}
4. Financial Reports
async function getProfitAndLoss(tenantId: string) {
const response = await xero.accountingApi.getReportProfitAndLoss(
tenantId,
'2026-01-01', // fromDate
'2026-03-31', // toDate
undefined, undefined, undefined,
'Month' // periods
);
return response.body.reports?.[0];
}
async function getBalanceSheet(tenantId: string) {
const response = await xero.accountingApi.getReportBalanceSheet(
tenantId,
'2026-03-31',
undefined, undefined, undefined,
'Month'
);
return response.body.reports?.[0];
}
5. Webhooks
import crypto from 'crypto';
const XERO_WEBHOOK_KEY = process.env.XERO_WEBHOOK_KEY!;
app.post('/webhooks/xero', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-xero-signature'] as string;
const payload = req.body.toString();
const hash = crypto
.createHmac('sha256', XERO_WEBHOOK_KEY)
.update(payload)
.digest('base64');
if (hash !== signature) {
// MUST return 401 — Xero will disable webhook after failures
return res.status(401).send();
}
const events = JSON.parse(payload);
for (const event of events.events) {
console.log(`${event.eventType} on ${event.resourceId} in tenant ${event.tenantId}`);
}
// MUST return 200 quickly — process async
res.status(200).send();
});
Best Practices
- Use
summarizeErrors=falsein batch creates to get per-item error details instead of a single 400. - Page large result sets — Use
pageparameter (100 items per page). Loop until you get fewer than 100. - Use
If-Modified-Sinceheader for sync — Only fetch contacts/invoices changed since your last sync. - Handle 429 rate limits — Xero allows 60 calls/minute per tenant. Use the
Retry-Afterheader. - Always specify
unitAmountwith tax-exclusive amounts whenlineAmountTypesisExclusive.
Common Pitfalls
- Missing
xero-tenant-idheader — Every API call needs it. Multi-org connections fail silently without it. - Using DRAFT status for invoices you want to reconcile — Only AUTHORISED or PAID invoices affect reports.
- Not handling disconnected tenants — Users can disconnect your app from specific orgs. Check
xero.tenantsafter token refresh. - Treating account codes as universal — Account codes vary per org. Query the Chart of Accounts to find correct codes.
Anti-Patterns
- Creating one API call per line item — Batch line items into a single invoice create call.
- Polling for changes — Use webhooks for real-time updates instead of polling endpoints.
- Storing Xero data as source of truth — Your app should reference Xero IDs but let Xero own the financial data.
- Ignoring validation errors array — Xero returns detailed validation errors in the response body. Always check
hasErrorson each entity.
Install this skill directly: skilldb add accounting-software-skills
Related Skills
FreeAgent API v2
You are a senior developer integrating with the FreeAgent API v2. You build integrations for UK freelancers and small businesses covering contacts, invoices, expenses, bank transactions, timeslips, an
FreshBooks API v3
You are a senior developer integrating with the FreshBooks API v3. You build integrations for client management, invoicing, expense tracking, time entries, and payments using OAuth 2.0 and FreshBooks'
KashFlow API
You are a senior developer integrating with the KashFlow API. You build integrations for UK small businesses covering customers, invoices, receipts, payments, bank accounts, and VAT returns using Kash
MYOB AccountRight API
You are a senior developer integrating with the MYOB AccountRight Live API. You build integrations for Australian/New Zealand businesses covering company files, contacts, invoices, payments, general j
Odoo Accounting XML-RPC / JSON-RPC API
You are a senior developer integrating with Odoo Accounting via its XML-RPC and JSON-RPC APIs. You build integrations for partners, invoices, payments, journal entries, reconciliation, and chart of ac
QuickBooks Online REST API v3
You are a senior developer integrating with the Intuit QuickBooks Online REST API v3. You build robust accounting integrations that create invoices, sync payments, manage customers/vendors, pull finan