🤖AI Agents Guide
TutorialsComparisonsReviewsExamplesIntegrationsUse CasesTemplatesGlossary
Get Started
🤖AI Agents Guide

Your comprehensive resource for understanding, building, and implementing AI Agents.

Learn

  • Tutorials
  • Glossary
  • Use Cases
  • Examples

Compare

  • Tool Comparisons
  • Reviews
  • Integrations
  • Templates

Company

  • About
  • Contact
  • Privacy Policy

© 2026 AI Agents Guide. All rights reserved.

Home/Integrations/How to Integrate AI Agents with Xero
IntegrationXerointermediate9 min readSetup: 25-35 minutes

How to Integrate AI Agents with Xero

Step-by-step guide to connecting AI agents with Xero. Learn how to automate financial reporting, invoice tracking, bank reconciliation, and expense analysis using LangChain, n8n, and the Xero API.

Pasta letters spell out "quero xero" on white background
Photo by Joao Vitor Marcilio on Unsplash
By AI Agents Guide Team•February 28, 2026

Table of Contents

  1. What AI Agents Can Do With Xero Access
  2. Setting Up Xero API Access
  3. Register a Xero App and Authenticate
  4. Option 1: No-Code with n8n
  5. Overdue Invoice Alert Workflow
  6. Option 2: LangChain with Python
  7. Build Xero Tools
  8. Xero Finance Agent
  9. Rate Limits and Best Practices
  10. Next Steps
a black and white photo of cubes on a black background
Photo by Shubham Dhage on Unsplash

Xero is the accounting platform of choice for millions of businesses across the UK, Australia, New Zealand, and growing rapidly in North America. Its clean REST API and comprehensive financial data model make it one of the best accounting platforms to connect to AI agents. With Xero integration, agents can deliver real-time financial visibility — turning what used to be a monthly accountant meeting into an on-demand conversation about your business finances.

For small business owners, finance managers, and anyone running a company on Xero, AI agent integration provides the financial intelligence layer that keeps decisions grounded in current numbers.

What AI Agents Can Do With Xero Access#

Financial Reporting

  • Generate Profit & Loss reports and Balance Sheets for any date range in plain language
  • Compare revenue and expense trends across months, quarters, or custom periods
  • Break down expenses by account category to identify cost optimization opportunities
  • Summarize cash position and net assets from the balance sheet on demand

Invoice and AR Management

  • List all outstanding invoices ranked by days overdue with customer contact details
  • Identify customers with invoices repeatedly past due for follow-up prioritization
  • Calculate total AR exposure across aging buckets (current, 30, 60, 90+ days)
  • Alert when new invoices are created without matching purchase orders

Bank and Cash Management

  • Check bank account balances across all connected accounts
  • List unreconciled bank transactions for review and matching
  • Flag transactions above a configurable dollar threshold for approval
  • Summarize cash inflows and outflows by source over any period

Setting Up Xero API Access#

pip install requests langchain langchain-openai python-dotenv

Register a Xero App and Authenticate#

  1. Go to developer.xero.com ↗ → Create a new app
  2. Set the redirect URI to http://localhost:8080/callback
  3. Note your Client ID and Client Secret
  4. Run the OAuth 2.0 Authorization Code flow to get your initial access and refresh tokens
  5. Call GET https://api.xero.com/connections to retrieve your Tenant ID (organization ID)
export XERO_CLIENT_ID="your-client-id"
export XERO_CLIENT_SECRET="your-client-secret"
export XERO_REFRESH_TOKEN="your-refresh-token"
export XERO_TENANT_ID="your-tenant-id"   # Organisation ID from /connections

Test your connection:

import os, requests

CLIENT_ID = os.getenv("XERO_CLIENT_ID")
CLIENT_SECRET = os.getenv("XERO_CLIENT_SECRET")
REFRESH_TOKEN = os.getenv("XERO_REFRESH_TOKEN")
TENANT_ID = os.getenv("XERO_TENANT_ID")

def get_xero_token() -> str:
    resp = requests.post(
        "https://identity.xero.com/connect/token",
        auth=(CLIENT_ID, CLIENT_SECRET),
        data={"grant_type": "refresh_token", "refresh_token": REFRESH_TOKEN}
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

token = get_xero_token()
resp = requests.get(
    "https://api.xero.com/api.xro/2.0/Organisation",
    headers={"Authorization": f"Bearer {token}", "Xero-tenant-id": TENANT_ID,
             "Accept": "application/json"}
)
org = resp.json()["Organisations"][0]
print(f"Connected to: {org['Name']} ({org['CountryCode']})")

Option 1: No-Code with n8n#

Overdue Invoice Alert Workflow#

  1. Schedule Trigger: Monday 9am
  2. HTTP Request: GET Xero Invoices API — Status=AUTHORISED&Where=AmountDue>0
  3. Code node: Filter invoices with DueDate more than 30 days ago, calculate days overdue
  4. OpenAI: "Write a summary of these overdue invoices for the finance team. Include total AR exposure and the top 5 most overdue customers."
  5. Slack/Email: Send to finance team channel with list of accounts to follow up

n8n has a native Xero OAuth2 credential type that handles token refresh automatically — configure it once and all Xero nodes will use it.


Option 2: LangChain with Python#

Build Xero Tools#

import os
import requests
from datetime import datetime, timedelta
from langchain.tools import tool
from dotenv import load_dotenv

load_dotenv()

CLIENT_ID = os.getenv("XERO_CLIENT_ID")
CLIENT_SECRET = os.getenv("XERO_CLIENT_SECRET")
REFRESH_TOKEN = os.getenv("XERO_REFRESH_TOKEN")
TENANT_ID = os.getenv("XERO_TENANT_ID")
XERO_BASE = "https://api.xero.com/api.xro/2.0"


def get_xero_token() -> str:
    """Exchange refresh token for fresh access token."""
    resp = requests.post(
        "https://identity.xero.com/connect/token",
        auth=(CLIENT_ID, CLIENT_SECRET),
        data={"grant_type": "refresh_token", "refresh_token": REFRESH_TOKEN}
    )
    resp.raise_for_status()
    return resp.json()["access_token"]


def xero_get(endpoint: str, params: dict = None) -> dict:
    """Execute an authenticated Xero API GET request."""
    token = get_xero_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Xero-tenant-id": TENANT_ID,
        "Accept": "application/json"
    }
    resp = requests.get(f"{XERO_BASE}/{endpoint}", headers=headers, params=params or {})
    resp.raise_for_status()
    return resp.json()


def xero_report(report_name: str, params: dict = None) -> dict:
    """Fetch a Xero financial report."""
    token = get_xero_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Xero-tenant-id": TENANT_ID,
        "Accept": "application/json"
    }
    resp = requests.get(
        f"{XERO_BASE}/Reports/{report_name}",
        headers=headers, params=params or {}
    )
    resp.raise_for_status()
    return resp.json()


@tool
def get_profit_loss(from_date: str, to_date: str) -> str:
    """
    Get Profit & Loss report for a date range.
    date format: 'YYYY-MM-DD'
    """
    data = xero_report("ProfitAndLoss", {
        "fromDate": from_date,
        "toDate": to_date,
        "standardLayout": "true"
    })
    reports = data.get("Reports", [])
    if not reports:
        return "No P&L data available"

    report = reports[0]
    lines = [f"Profit & Loss ({from_date} to {to_date}):"]

    for section in report.get("Rows", []):
        row_type = section.get("RowType", "")
        if row_type == "Section":
            title = section.get("Title", "")
            if title:
                lines.append(f"\n{title}:")
            for row in section.get("Rows", []):
                cells = row.get("Cells", [])
                if len(cells) >= 2:
                    label = cells[0].get("Value", "")
                    amount = cells[1].get("Value", "0")
                    if label and amount and amount != "":
                        try:
                            lines.append(f"  {label}: ${float(amount):,.2f}")
                        except ValueError:
                            pass
        elif row_type == "SummaryRow":
            cells = section.get("Cells", [])
            if len(cells) >= 2:
                label = cells[0].get("Value", "")
                amount = cells[1].get("Value", "0")
                try:
                    lines.append(f"\n{label}: ${float(amount):,.2f}")
                except ValueError:
                    pass

    return "\n".join(lines)


@tool
def get_outstanding_invoices(min_days_overdue: int = 0) -> str:
    """
    Get unpaid invoices from Xero ranked by days overdue.
    min_days_overdue: only show invoices overdue by at least this many days.
    """
    data = xero_get("Invoices", {
        "Status": "AUTHORISED",
        "where": "AmountDue>0",
        "order": "DueDate ASC"
    })
    invoices = data.get("Invoices", [])

    if not invoices:
        return "No outstanding invoices found"

    today = datetime.today().date()
    lines = []
    total = 0.0

    for inv in invoices[:50]:
        contact_name = inv.get("Contact", {}).get("Name", "Unknown")
        amount_due = float(inv.get("AmountDue", 0))
        due_date_str = inv.get("DueDateString", "")
        inv_number = inv.get("InvoiceNumber", "N/A")
        days_overdue = 0

        if due_date_str:
            try:
                due_date = datetime.strptime(due_date_str[:10], "%Y-%m-%d").date()
                days_overdue = (today - due_date).days
            except ValueError:
                pass

        if days_overdue >= min_days_overdue:
            status = f"{days_overdue}d overdue" if days_overdue > 0 else f"due {due_date_str[:10]}"
            lines.append(f"  {inv_number} | {contact_name} | ${amount_due:,.2f} | {status}")
            total += amount_due

    if not lines:
        return f"No invoices overdue by {min_days_overdue}+ days"

    return (f"Outstanding invoices ({len(lines)} found):\n" +
            "\n".join(lines) +
            f"\nTotal AR: ${total:,.2f}")


@tool
def get_bank_account_balances() -> str:
    """Get current balances for all Xero bank accounts."""
    data = xero_get("Accounts", {"Type": "BANK", "where": "Status==\"ACTIVE\""})
    accounts = data.get("Accounts", [])

    if not accounts:
        return "No bank accounts found"

    lines = ["Bank account balances:"]
    total = 0.0
    for account in accounts:
        name = account.get("Name", "Unknown")
        balance = float(account.get("CurrencyCode", "0") and
                       account.get("ReportingCodeUpdatedDateUTC", "0") and
                       "0")  # Balance requires separate API call
        code = account.get("Code", "")
        currency = account.get("CurrencyCode", "")
        lines.append(f"  {name} ({code}): {currency} — use Bank Summary report for balances")

    # Get actual balances from BankSummary report
    try:
        summary = xero_report("BankSummary")
        reports = summary.get("Reports", [])
        if reports:
            lines = ["Bank account balances:"]
            total = 0.0
            for row in reports[0].get("Rows", []):
                cells = row.get("Cells", [])
                if len(cells) >= 5:
                    account_name = cells[0].get("Value", "")
                    closing_balance = cells[4].get("Value", "0")
                    if account_name and closing_balance:
                        try:
                            bal = float(closing_balance)
                            lines.append(f"  {account_name}: ${bal:,.2f}")
                            total += bal
                        except ValueError:
                            pass
            lines.append(f"\nTotal cash: ${total:,.2f}")
    except Exception:
        pass

    return "\n".join(lines)


@tool
def get_aged_receivables() -> str:
    """Get aged receivables report showing outstanding amounts by aging bucket."""
    data = xero_report("AgedReceivablesByContact")
    reports = data.get("Reports", [])
    if not reports:
        return "No aged receivables data available"

    lines = ["Aged Receivables Summary:"]
    report = reports[0]

    for row in report.get("Rows", []):
        if row.get("RowType") == "Row":
            cells = row.get("Cells", [])
            if len(cells) >= 6:
                contact = cells[0].get("Value", "")
                current = cells[1].get("Value", "0")
                days_30 = cells[2].get("Value", "0")
                days_60 = cells[3].get("Value", "0")
                days_90 = cells[4].get("Value", "0")
                total = cells[5].get("Value", "0")
                try:
                    if float(total) > 0:
                        lines.append(f"  {contact}: Current ${float(current):,.0f} | "
                                     f"30d ${float(days_30):,.0f} | "
                                     f"60d ${float(days_60):,.0f} | "
                                     f"90d+ ${float(days_90):,.0f} | "
                                     f"Total ${float(total):,.0f}")
                except ValueError:
                    pass

    return "\n".join(lines) if len(lines) > 1 else "No outstanding receivables"


@tool
def get_expense_claims() -> str:
    """Get pending employee expense claims awaiting approval."""
    data = xero_get("ExpenseClaims", {"Status": "SUBMITTED"})
    claims = data.get("ExpenseClaims", [])

    if not claims:
        return "No pending expense claims"

    lines = [f"Pending expense claims ({len(claims)}):"]
    total = 0.0
    for claim in claims:
        user = claim.get("User", {})
        name = f"{user.get('FirstName', '')} {user.get('LastName', '')}".strip()
        amount = float(claim.get("AmountDue", 0))
        status = claim.get("Status", "")
        total += amount
        lines.append(f"  {name} | ${amount:,.2f} | {status}")

    lines.append(f"\nTotal pending: ${total:,.2f}")
    return "\n".join(lines)

Xero Finance Agent#

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [get_profit_loss, get_outstanding_invoices, get_bank_account_balances,
         get_aged_receivables, get_expense_claims]

prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a financial operations assistant with access to Xero accounting.

When answering financial questions:
1. Specify date ranges clearly for all reports
2. Rank overdue invoices by urgency (90+ days is critical, 60+ is high priority)
3. Present currency values with $ and comma formatting: $12,345.67
4. Highlight patterns that suggest cash flow risk or collection problems
5. Recommend one specific action based on the data"""),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=6)

Rate Limits and Best Practices#

Xero API limitValue
Requests per minute60
Daily request limit5,000
Refresh token expiry60 days (custom apps)
Access token expiry30 minutes

Best practices:

  • Handle token refresh proactively: Access tokens expire in 30 minutes — fetch a fresh token at the start of each agent tool call rather than caching across a long session
  • Use where filtering: Xero supports server-side filtering with where=Status=="AUTHORISED" — always filter at the API level rather than fetching all records and filtering client-side
  • Tenant ID required on every request: All Xero API calls must include the Xero-tenant-id header — store this alongside your credentials and never omit it
  • Archive stale contacts: Before running AR analysis, filter out ARCHIVED invoices using Status=AUTHORISED to avoid noise from historical data

Next Steps#

  • AI Agents QuickBooks Integration — North America's leading accounting platform for the same use cases
  • AI Agents Stripe Integration — Connect payment processing data with Xero AR tracking
  • AI Agents Slack Integration — Send overdue invoice alerts and financial digests to Slack
  • Build an AI Agent with LangChain — Complete agent framework tutorial

Related Integrations

How to Integrate AI Agents with Airtable

Step-by-step guide to connecting AI agents with Airtable. Learn how to automate record creation, data enrichment, workflow triggers, and database management using LangChain, n8n, and the Airtable REST API.

How to Integrate AI Agents with Asana

Step-by-step guide to connecting AI agents with Asana. Learn how to automate task creation, project updates, workload analysis, and deadline tracking using LangChain, n8n, and the Asana REST API.

AI Agents + Google BigQuery: Setup Guide

Step-by-step guide to connecting AI agents with Google BigQuery. Learn how to automate SQL queries, build analytics pipelines, detect anomalies, and generate business reports using LangChain, n8n, and the BigQuery Python SDK.

← Back to All Integrations