Xero API Integration Skill
Version: 1.0.0 Author: Claude Code Purpose: Integrate with Xero Accounting API for invoice and contact management
Overview and Purpose
This skill provides comprehensive guidance for integrating with the Xero Accounting API. It helps you:
- Set up OAuth 2.0 authentication with proper scopes
- Manage access tokens and refresh tokens
- Create and manage invoices
- Manage contacts (customers and suppliers)
- Handle rate limits (5,000/day, 10,000/minute)
- Query reports and financial data
Important: This skill provides guidance and patterns. Actual API calls require a registered Xero application with valid credentials stored securely (use the 1password-secrets skill).
Prerequisites
Required
-
Xero Developer Account
- Sign up at https://developer.xero.com
- Enable demo company for testing
-
Registered OAuth 2.0 Application
1. Go to https://developer.xero.com/app/manage 2. Click "New app" 3. Choose "Web app" for server-side integration 4. Configure redirect URI (e.g., http://localhost:3000/callback) 5. Save Client ID and Client Secret securely -
Secure Credential Storage
# Store credentials in 1Password (recommended) op item create --category=API_Credential \ --title="Xero-OAuth" \ --vault="ProjectSecrets" \ client_id=<your-client-id> \ client_secret=<your-client-secret>
Environment Template
Create .env.template with 1Password references:
# Xero OAuth2 Credentials
XERO_CLIENT_ID=op://ProjectSecrets/Xero-OAuth/client_id
XERO_CLIENT_SECRET=op://ProjectSecrets/Xero-OAuth/client_secret
XERO_REDIRECT_URI=http://localhost:3000/callback
# Token Storage (use secure storage in production)
XERO_ACCESS_TOKEN=op://ProjectSecrets/Xero-Tokens/access_token
XERO_REFRESH_TOKEN=op://ProjectSecrets/Xero-Tokens/refresh_token
XERO_TENANT_ID=op://ProjectSecrets/Xero-Tokens/tenant_id
Workflow 1: OAuth 2.0 Setup
Purpose: Establish secure authentication with Xero API.
When to Use
- Initial application setup
- After credential rotation
- When adding new organization connections
OAuth 2.0 Flow
1. User Authorization
----------------------
Redirect user to Xero authorization URL:
https://login.xero.com/identity/connect/authorize?
response_type=code&
client_id=<CLIENT_ID>&
redirect_uri=<REDIRECT_URI>&
scope=openid profile email offline_access accounting.transactions accounting.contacts&
state=<RANDOM_STATE>
2. Authorization Callback
----------------------
User returns with authorization code:
GET <REDIRECT_URI>?code=<AUTH_CODE>&state=<STATE>
3. Token Exchange
----------------------
POST https://identity.xero.com/connect/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(<CLIENT_ID>:<CLIENT_SECRET>)
grant_type=authorization_code&
code=<AUTH_CODE>&
redirect_uri=<REDIRECT_URI>
4. Response
----------------------
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 1800,
"token_type": "Bearer",
"refresh_token": "abc123...",
"scope": "openid profile email offline_access accounting.transactions"
}
Scopes Reference
| Scope | Access Level |
|-------|-------------|
| openid | OpenID Connect authentication |
| profile | User profile information |
| email | User email address |
| offline_access | Refresh tokens (required for token refresh) |
| accounting.transactions | Invoices, bills, bank transactions |
| accounting.transactions.read | Read-only transactions |
| accounting.contacts | Contacts (customers/suppliers) |
| accounting.contacts.read | Read-only contacts |
| accounting.reports.read | Financial reports |
| accounting.settings | Organization settings |
| accounting.settings.read | Read-only settings |
| accounting.attachments | File attachments |
Security Considerations
- Store credentials in 1Password, never in code
- Use HTTPS for all redirect URIs in production
- Validate
stateparameter to prevent CSRF - Access tokens expire in 30 minutes; implement refresh
Workflow 2: Token Management
Purpose: Handle token refresh and storage securely.
Token Refresh Flow
Access tokens expire after 30 minutes.
Refresh before expiry to maintain session.
POST https://identity.xero.com/connect/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(<CLIENT_ID>:<CLIENT_SECRET>)
grant_type=refresh_token&
refresh_token=<REFRESH_TOKEN>
Response:
{
"access_token": "new_access_token...",
"expires_in": 1800,
"token_type": "Bearer",
"refresh_token": "new_refresh_token...",
"scope": "..."
}
Token Storage Pattern
# Python example using 1Password CLI
import subprocess
import json
from datetime import datetime, timedelta
class XeroTokenManager:
def __init__(self, vault="ProjectSecrets", item="Xero-Tokens"):
self.vault = vault
self.item = item
self._token_expiry = None
def get_access_token(self):
"""Get current access token, refreshing if needed."""
if self._is_token_expired():
self._refresh_token()
result = subprocess.run(
["op", "item", "get", self.item,
"--vault", self.vault,
"--fields", "access_token"],
capture_output=True, text=True
)
return result.stdout.strip()
def _is_token_expired(self):
"""Check if token needs refresh (5 min buffer)."""
if self._token_expiry is None:
return True
return datetime.now() >= self._token_expiry - timedelta(minutes=5)
def _refresh_token(self):
"""Refresh access token using refresh token."""
# Implementation: call Xero token endpoint
# Update tokens in 1Password
pass
Best Practices
- Refresh proactively - Refresh 5 minutes before expiry
- Handle refresh failures - Implement re-authorization flow
- Store securely - Use 1Password or similar secret manager
- Log token events - Track refresh attempts for debugging
Workflow 3: Invoice Management
Purpose: Create, retrieve, and manage invoices.
When to Use
- Creating sales invoices
- Querying invoice status
- Sending invoices to customers
- Managing invoice payments
Create Invoice
POST https://api.xero.com/api.xro/2.0/Invoices
Authorization: Bearer <ACCESS_TOKEN>
xero-tenant-id: <TENANT_ID>
Content-Type: application/json
{
"Type": "ACCREC",
"Contact": {
"ContactID": "a1b2c3d4-..."
},
"LineItems": [
{
"Description": "Consulting Services",
"Quantity": 10,
"UnitAmount": 150.00,
"AccountCode": "200"
}
],
"Date": "2026-01-10",
"DueDate": "2026-02-10",
"Reference": "INV-001",
"Status": "DRAFT"
}
Invoice Types
| Type | Description |
|------|-------------|
| ACCREC | Accounts Receivable (Sales Invoice) |
| ACCPAY | Accounts Payable (Bill) |
Invoice Statuses
| Status | Description | Transitions |
|--------|-------------|-------------|
| DRAFT | Not yet approved | -> SUBMITTED, AUTHORISED, DELETED |
| SUBMITTED | Awaiting approval | -> AUTHORISED, DELETED |
| AUTHORISED | Approved, awaiting payment | -> PAID, VOIDED |
| PAID | Fully paid | (final state) |
| VOIDED | Cancelled | (final state) |
| DELETED | Removed | (final state) |
Bulk Operations
Create up to 50 invoices per request (3.5MB max).
Counts as single API call.
POST https://api.xero.com/api.xro/2.0/Invoices
{
"Invoices": [
{ "Type": "ACCREC", ... },
{ "Type": "ACCREC", ... },
...
]
}
Query Invoices
GET https://api.xero.com/api.xro/2.0/Invoices
Authorization: Bearer <ACCESS_TOKEN>
xero-tenant-id: <TENANT_ID>
Optional parameters:
- where: Filter expression (e.g., Status=="AUTHORISED")
- order: Sort order (e.g., UpdatedDateUTC DESC)
- page: Page number for pagination
- summaryOnly: true for smaller response
Common Patterns
Get overdue invoices:
GET /Invoices?where=Status=="AUTHORISED"AND AmountDue>0 AND DueDate<DateTime(2026,01,10)
Get recent invoices:
GET /Invoices?where=UpdatedDateUTC>=DateTime(2026,01,01)&order=UpdatedDateUTC DESC
Workflow 4: Contact Management
Purpose: Manage customers and suppliers.
When to Use
- Adding new customers
- Updating supplier information
- Syncing contacts from CRM
- Querying contact details
Create Contact
POST https://api.xero.com/api.xro/2.0/Contacts
Authorization: Bearer <ACCESS_TOKEN>
xero-tenant-id: <TENANT_ID>
Content-Type: application/json
{
"Name": "Acme Corporation",
"FirstName": "John",
"LastName": "Smith",
"EmailAddress": "john.smith@acme.com",
"Phones": [
{
"PhoneType": "DEFAULT",
"PhoneNumber": "555-1234"
}
],
"Addresses": [
{
"AddressType": "STREET",
"AddressLine1": "123 Main Street",
"City": "San Francisco",
"Region": "CA",
"PostalCode": "94105",
"Country": "USA"
}
],
"IsCustomer": true,
"IsSupplier": false
}
Query Contacts
GET https://api.xero.com/api.xro/2.0/Contacts
Authorization: Bearer <ACCESS_TOKEN>
xero-tenant-id: <TENANT_ID>
Optional parameters:
- where: Filter expression
- order: Sort order
- page: Page number
- summaryOnly: true for smaller response
- includeArchived: true to include archived contacts
Contact Types
| Field | Values | Description |
|-------|--------|-------------|
| IsCustomer | true/false | Receives invoices |
| IsSupplier | true/false | Sends bills |
| Both can be true | | Contact is both |
Workflow 5: Rate Limit Management
Purpose: Stay within API limits and handle throttling gracefully.
Xero Rate Limits
| Limit Type | Value | Reset | |------------|-------|-------| | Daily per organization | 5,000 calls | Midnight UTC | | Minute per organization | 10,000 calls | Rolling 60 seconds | | Concurrent connections | 60 | Per integration |
Response Headers
X-Rate-Limit-Problem: daily
Retry-After: 3600
X-MinLimit-Remaining: 9500
X-DayLimit-Remaining: 4800
Implementation Pattern
import time
from functools import wraps
def rate_limit_handler(func):
@wraps(func)
def wrapper(*args, **kwargs):
max_retries = 3
for attempt in range(max_retries):
response = func(*args, **kwargs)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
return response
raise Exception("Max retries exceeded")
return wrapper
Best Practices
- Monitor limits - Check X-DayLimit-Remaining header
- Use pagination - Reduce calls with larger page sizes
- Batch operations - Use bulk endpoints when possible
- Cache responses - Store frequently accessed data
- Implement backoff - Exponential backoff for retries
Workflow 6: Multi-Tenant Support
Purpose: Handle multiple Xero organizations.
When to Use
- Accounting firms managing multiple clients
- SaaS platforms with multiple users
- Organizations with multiple entities
Tenant Discovery
After OAuth, query connected tenants:
GET https://api.xero.com/connections
Authorization: Bearer <ACCESS_TOKEN>
Response:
[
{
"id": "abc123...",
"tenantId": "tenant-uuid-1",
"tenantType": "ORGANISATION",
"tenantName": "Demo Company (US)"
},
{
"id": "def456...",
"tenantId": "tenant-uuid-2",
"tenantType": "ORGANISATION",
"tenantName": "Client Company Ltd"
}
]
Making Tenant-Specific Calls
Always include tenant ID header:
GET https://api.xero.com/api.xro/2.0/Invoices
Authorization: Bearer <ACCESS_TOKEN>
xero-tenant-id: <TENANT_ID>
Best Practices
- Store tenant mappings - Map users to tenant IDs
- Handle disconnections - Users can revoke access
- Per-tenant rate limits - Limits apply per organization
Workflow 7: Webhooks
Purpose: Receive real-time notifications of data changes.
When to Use
- Real-time invoice status updates
- Contact synchronization
- Payment notifications
- Audit logging
Webhook Setup
- Register webhook URL in Xero Developer Portal
- Verify webhook with intent validation
- Process events asynchronously
Webhook Payload
{
"events": [
{
"resourceUrl": "https://api.xero.com/api.xro/2.0/Invoices/abc123",
"resourceId": "abc123",
"tenantId": "tenant-uuid",
"tenantType": "ORGANISATION",
"eventCategory": "INVOICE",
"eventType": "UPDATE",
"eventDateUtc": "2026-01-10T12:00:00Z"
}
],
"firstEventSequence": 1,
"lastEventSequence": 1
}
Event Types
| Category | Event Types |
|----------|-------------|
| INVOICE | CREATE, UPDATE |
| CONTACT | CREATE, UPDATE |
| PAYMENT | CREATE, UPDATE |
| CREDITNOTE | CREATE, UPDATE |
Security
- Validate signature - Verify webhook HMAC signature
- Respond quickly - Return 2xx within 5 seconds
- Process async - Queue events for processing
- Handle duplicates - Events may be sent multiple times
Common Patterns
Pattern 1: Sync Invoices to External System
def sync_invoices_since(last_sync_date):
"""Fetch invoices modified since last sync."""
page = 1
while True:
response = xero_api.get(
"/Invoices",
params={
"where": f"UpdatedDateUTC>=DateTime({last_sync_date})",
"page": page
}
)
invoices = response.json()["Invoices"]
if not invoices:
break
for invoice in invoices:
process_invoice(invoice)
page += 1
Pattern 2: Create Invoice from Order
def create_invoice_from_order(order, contact_id):
"""Convert order to Xero invoice."""
line_items = [
{
"Description": item["name"],
"Quantity": item["quantity"],
"UnitAmount": item["price"],
"AccountCode": "200" # Sales account
}
for item in order["items"]
]
return xero_api.post("/Invoices", json={
"Type": "ACCREC",
"Contact": {"ContactID": contact_id},
"LineItems": line_items,
"Date": order["date"],
"DueDate": order["due_date"],
"Reference": order["reference"],
"Status": "AUTHORISED" # Ready for payment
})
Pattern 3: Find or Create Contact
def find_or_create_contact(email, name):
"""Get existing contact by email or create new."""
# Search by email
response = xero_api.get(
"/Contacts",
params={"where": f'EmailAddress=="{email}"'}
)
contacts = response.json()["Contacts"]
if contacts:
return contacts[0]["ContactID"]
# Create new contact
response = xero_api.post("/Contacts", json={
"Name": name,
"EmailAddress": email,
"IsCustomer": True
})
return response.json()["Contacts"][0]["ContactID"]
Troubleshooting
Issue 1: 401 Unauthorized
Symptoms:
{"Type": "OAuth2", "Detail": "The access token has expired"}
Solution:
- Check token expiry (30 minutes)
- Implement automatic token refresh
- Verify refresh token is still valid
Issue 2: 403 Forbidden
Symptoms:
{"Type": "NoPermissions", "Detail": "You do not have permission"}
Solution:
- Verify required scopes are requested
- Check user has necessary permissions in Xero
- Re-authorize with correct scopes
Issue 3: 429 Rate Limited
Symptoms:
HTTP 429 Too Many Requests
X-Rate-Limit-Problem: daily
Solution:
- Check X-DayLimit-Remaining header
- Implement exponential backoff
- Use bulk endpoints where possible
- Wait for daily reset at midnight UTC
Issue 4: Invalid Tenant ID
Symptoms:
{"Type": "InvalidTenantId"}
Solution:
- Query /connections to get valid tenant IDs
- Verify user has access to the organization
- Check for tenant disconnection
Issue 5: Validation Errors
Symptoms:
{
"Type": "ValidationException",
"Message": "A validation exception occurred",
"Elements": [
{
"ValidationErrors": [
{"Message": "Contact Name must be specified"}
]
}
]
}
Solution:
- Check required fields in documentation
- Validate data before API call
- Handle validation errors gracefully
Security Best Practices
- Never commit credentials - Use 1Password templates
- Use HTTPS - All redirect URIs in production
- Validate state - Prevent CSRF in OAuth flow
- Limit scopes - Request only needed permissions
- Rotate secrets - Regular credential rotation
- Audit access - Log API calls and token usage
- Handle token revocation - Users can disconnect
Quick Reference
API Base URLs
| Environment | URL |
|-------------|-----|
| Production | https://api.xero.com/api.xro/2.0/ |
| Identity | https://identity.xero.com/ |
| OAuth | https://login.xero.com/identity/connect/ |
Required Headers
Authorization: Bearer <ACCESS_TOKEN>
xero-tenant-id: <TENANT_ID>
Content-Type: application/json
Accept: application/json
Common Status Codes
| Code | Meaning | |------|---------| | 200 | Success | | 201 | Created | | 400 | Bad Request | | 401 | Unauthorized (token expired) | | 403 | Forbidden (insufficient scope) | | 404 | Not Found | | 429 | Rate Limited | | 500 | Server Error |
Related Skills
- 1password-secrets: Secure credential storage
- devops: CI/CD integration with Xero
References
Version History:
- 1.0.0 (2026-01-10): Initial release with OAuth2, invoices, contacts workflows
Maintainer: Claude Code License: Apache-2.0