Claude
Skills
Sign in
Back

xero-accounting

Included with Lifetime
$97 forever

Integrate with the Xero accounting API to sync invoices, expenses, bank transactions, and contacts — and generate financial reports like P&L and balance sheet. Use when: connecting apps to Xero, automating bookkeeping workflows, syncing accounting data, or pulling financial reports programmatically.

Backend & APIs

What this skill does


# Xero Accounting

## Overview

Integrate your application with Xero's accounting platform via the Xero API. This skill covers OAuth2 authentication, syncing core accounting objects (invoices, bills, expenses, contacts, bank transactions), and pulling financial reports (P&L, balance sheet, trial balance). Supports both one-time imports and continuous sync patterns.

## Instructions

### Step 1: Set up OAuth2 authentication

Xero uses OAuth2 with PKCE. Register your app at [developer.xero.com](https://developer.xero.com), then implement the token flow:

```python
# xero_auth.py — OAuth2 token management for Xero API
import requests
import base64
import json
import os
from datetime import datetime, timedelta

XERO_CLIENT_ID = os.environ["XERO_CLIENT_ID"]
XERO_CLIENT_SECRET = os.environ["XERO_CLIENT_SECRET"]
XERO_REDIRECT_URI = os.environ["XERO_REDIRECT_URI"]
TOKEN_FILE = ".xero_tokens.json"

def get_auth_url():
    """Generate the OAuth2 authorization URL."""
    import secrets
    state = secrets.token_urlsafe(16)
    params = {
        "response_type": "code",
        "client_id": XERO_CLIENT_ID,
        "redirect_uri": XERO_REDIRECT_URI,
        "scope": "openid profile email accounting.transactions accounting.reports.read accounting.contacts offline_access",
        "state": state,
    }
    query = "&".join(f"{k}={v}" for k, v in params.items())
    return f"https://login.xero.com/identity/connect/authorize?{query}"

def exchange_code_for_tokens(code: str) -> dict:
    """Exchange authorization code for access + refresh tokens."""
    credentials = base64.b64encode(
        f"{XERO_CLIENT_ID}:{XERO_CLIENT_SECRET}".encode()
    ).decode()

    response = requests.post(
        "https://identity.xero.com/connect/token",
        headers={
            "Authorization": f"Basic {credentials}",
            "Content-Type": "application/x-www-form-urlencoded",
        },
        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": XERO_REDIRECT_URI,
        },
    )
    tokens = response.json()
    tokens["expires_at"] = (
        datetime.utcnow() + timedelta(seconds=tokens["expires_in"])
    ).isoformat()
    save_tokens(tokens)
    return tokens

def refresh_access_token(tokens: dict) -> dict:
    """Refresh the access token using the refresh token."""
    credentials = base64.b64encode(
        f"{XERO_CLIENT_ID}:{XERO_CLIENT_SECRET}".encode()
    ).decode()

    response = requests.post(
        "https://identity.xero.com/connect/token",
        headers={
            "Authorization": f"Basic {credentials}",
            "Content-Type": "application/x-www-form-urlencoded",
        },
        data={
            "grant_type": "refresh_token",
            "refresh_token": tokens["refresh_token"],
        },
    )
    new_tokens = response.json()
    new_tokens["expires_at"] = (
        datetime.utcnow() + timedelta(seconds=new_tokens["expires_in"])
    ).isoformat()
    save_tokens(new_tokens)
    return new_tokens

def get_valid_token() -> str:
    """Return a valid access token, refreshing if needed."""
    tokens = load_tokens()
    expires_at = datetime.fromisoformat(tokens["expires_at"])
    if datetime.utcnow() >= expires_at - timedelta(minutes=5):
        tokens = refresh_access_token(tokens)
    return tokens["access_token"]

def get_tenant_id(access_token: str) -> str:
    """Get the Xero organisation (tenant) ID."""
    response = requests.get(
        "https://api.xero.com/connections",
        headers={"Authorization": f"Bearer {access_token}"},
    )
    connections = response.json()
    return connections[0]["tenantId"]  # Use first connected org

def save_tokens(tokens: dict):
    with open(TOKEN_FILE, "w") as f:
        json.dump(tokens, f)

def load_tokens() -> dict:
    with open(TOKEN_FILE) as f:
        return json.load(f)
```

### Step 2: Create an API client

```python
# xero_client.py — Reusable Xero API client
import requests
from xero_auth import get_valid_token, get_tenant_id

class XeroClient:
    BASE_URL = "https://api.xero.com/api.xro/2.0"

    def __init__(self):
        self.token = get_valid_token()
        self.tenant_id = get_tenant_id(self.token)

    def _headers(self):
        return {
            "Authorization": f"Bearer {self.token}",
            "Xero-Tenant-Id": self.tenant_id,
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

    def get(self, endpoint: str, params: dict = None) -> dict:
        response = requests.get(
            f"{self.BASE_URL}/{endpoint}",
            headers=self._headers(),
            params=params,
        )
        response.raise_for_status()
        return response.json()

    def post(self, endpoint: str, data: dict) -> dict:
        response = requests.post(
            f"{self.BASE_URL}/{endpoint}",
            headers=self._headers(),
            json=data,
        )
        response.raise_for_status()
        return response.json()

    def put(self, endpoint: str, data: dict) -> dict:
        response = requests.put(
            f"{self.BASE_URL}/{endpoint}",
            headers=self._headers(),
            json=data,
        )
        response.raise_for_status()
        return response.json()
```

### Step 3: Sync invoices

```python
# sync_invoices.py — Create and retrieve invoices in Xero
from xero_client import XeroClient
from datetime import datetime

client = XeroClient()

def create_invoice(contact_name: str, line_items: list, due_date: str, currency: str = "USD") -> dict:
    """
    Create a sales invoice (ACCREC) in Xero.

    line_items format:
      [{"description": "...", "quantity": 1, "unitAmount": 100.0, "accountCode": "200"}]
    """
    payload = {
        "Invoices": [{
            "Type": "ACCREC",
            "Contact": {"Name": contact_name},
            "DueDate": due_date,          # e.g. "2025-03-31"
            "CurrencyCode": currency,
            "LineItems": [
                {
                    "Description": item["description"],
                    "Quantity": item["quantity"],
                    "UnitAmount": item["unitAmount"],
                    "AccountCode": item.get("accountCode", "200"),
                }
                for item in line_items
            ],
            "Status": "AUTHORISED",
        }]
    }
    result = client.post("Invoices", payload)
    invoice = result["Invoices"][0]
    print(f"Created invoice {invoice['InvoiceNumber']} — Total: {invoice['Total']}")
    return invoice

def list_invoices(status: str = "AUTHORISED", modified_since: str = None) -> list:
    """Retrieve invoices, optionally filtered by status or modified date."""
    params = {"Status": status}
    headers_extra = {}
    if modified_since:
        headers_extra["If-Modified-Since"] = modified_since

    result = client.get("Invoices", params=params)
    return result.get("Invoices", [])

def mark_invoice_paid(invoice_id: str, amount: float, account_code: str = "090") -> dict:
    """Record a payment against an invoice."""
    payload = {
        "Payments": [{
            "Invoice": {"InvoiceID": invoice_id},
            "Account": {"Code": account_code},
            "Amount": amount,
            "Date": datetime.utcnow().strftime("%Y-%m-%d"),
        }]
    }
    result = client.post("Payments", payload)
    return result["Payments"][0]
```

### Step 4: Sync bank transactions

```python
# sync_bank_transactions.py — Push bank transactions into Xero
from xero_client import XeroClient

client = XeroClient()

def create_bank_transaction(
    account_id: str,
    contact_name: str,
    amount: float,
    date: str,
    description: str,
    account_code: str = "400",
    tx_type: str = "SPEND",  # SPEND or RECEIVE
) -> dict:
    """Record a bank transaction (spend or receive money)."""
    payload = {
        "BankTransactions": [{
            "Type": tx_type,
            "Contact": {"Name": contact_name},
            "BankAccount": {"AccountID"

Related in Backend & APIs