---
title: "How to Connect AI Agents to Xero and QuickBooks: MCP Server Architecture Guide"
slug: how-to-connect-ai-agents-to-xero-and-quickbooks-mcp-server-architecture-guide
date: 2026-04-07
author: Sidharth Verma
categories: ["AI & Agents", Engineering, Guides]
excerpt: "Learn how to connect AI agents to Xero, QuickBooks, and Brex using an MCP server architecture. Master OAuth concurrency, rate limits, and schema normalization."
tldr: "Exposing Xero, QuickBooks, and Brex to AI agents requires an MCP server that standardizes tool calling, normalizes API rate limits, and manages OAuth concurrency using durable locks."
canonical: https://truto.one/blog/how-to-connect-ai-agents-to-xero-and-quickbooks-mcp-server-architecture-guide/
---

# How to Connect AI Agents to Xero and QuickBooks: MCP Server Architecture Guide


If you need an AI agent to read invoices from Xero, create bills in QuickBooks Online, pull expense data from Brex, or reconcile transactions across all three—without building and maintaining separate API connectors for every AI model your customers use—this is the architecture guide you need.

Connecting AI agents to enterprise accounting systems is a nightmare of state management, strict schemas, and aggressive rate limits. If you are a product manager or engineering leader tasked with giving an LLM read and write access to financial data, you already know that brute-forcing point-to-point [accounting integrations](https://truto.one/blog/what-are-accounting-integrations-2026-architecture-strategy-guide/) is an unscalable approach. 

Search intent dictates we answer the core architectural question immediately: **To connect AI agents to Xero and QuickBooks securely and at scale, you must deploy an MCP (Model Context Protocol) server backed by a unified accounting schema.** This architecture translates the agent's natural language intent into standardized JSON-RPC tool calls, normalizes the disparate data models of different ERPs, and passes strict API rate limits back to the agent so it can manage its own execution backoff.

The demand for this architecture is massive. A January 2026 Deloitte study found that 63% of finance organizations have fully deployed AI in their operations, and nearly 50% of CFOs report having integrated AI-driven agents into parts of the finance function like forecasting or expense management. According to PwC, over 79% of finance leaders believe AI agents are already changing how financial data is analyzed. The accuracy gains are tangible: in recent testing, AI bookkeeping agents demonstrated 97.8% accuracy in automated transaction categorization, compared to 79.1% for human outsourced accountants across real workflows. Goldman Sachs is already deploying agentic workflows for transaction reconciliation and trade accounting.

AI agents can automate up to 95% of the entire bookkeeping workflow—but only if your underlying infrastructure can reliably handle the API layer. This guide breaks down the exact architecture required to build a production-ready MCP server for QuickBooks Online and Xero, covering OAuth concurrency, rate limit normalization, schema mapping, and pagination.

## The N×M Problem of Connecting AI Agents to Accounting APIs

Historically, exposing a SaaS product to an AI model meant building custom connectors for OpenAI, Anthropic, Google, and whatever LangChain or LlamaIndex wrapper your enterprise customers decided to use. If you support five AI models and ten accounting systems, your engineering team is responsible for maintaining fifty separate integration paths.

Accounting APIs compound this N×M problem. Double-entry ledgers, strict tax rate dependencies, and multi-currency edge cases mean that a failed write operation doesn't just throw an error—it corrupts a financial record. Each API has its own OAuth flow, its own pagination style, its own field names for the same concept (Xero calls them "Contacts," QuickBooks calls them "Customers" and "Vendors" separately), and its own rate limit scheme.

The [Model Context Protocol (MCP)](https://truto.one/blog/what-is-mcp-model-context-protocol-the-2026-guide-for-saas-pms/) acts as the universal adapter that collapses this complexity. Anthropic launched MCP in November 2024, and the adoption curve has been staggering. By March 2026, the protocol hit 97 million monthly SDK downloads across Python and TypeScript, with first-class client support in Claude, ChatGPT, Gemini, Cursor, and VS Code. In December 2025, Anthropic donated MCP to the Agentic AI Foundation under the Linux Foundation, with OpenAI, Google, Microsoft, and AWS as backing members.

By deploying an [MCP server](https://truto.one/blog/what-is-an-mcp-server-the-2026-architecture-guide-for-saas-pms/), you build the accounting integration layer exactly once. Any MCP-compatible client—whether it is Claude Desktop, a custom LangGraph agent, or an internal support bot—can immediately discover and execute tools against Xero and QuickBooks without a single line of model-specific glue code.

## How an MCP Server Architecture Works for Xero and QuickBooks

**An MCP server for accounting is a JSON-RPC 2.0 endpoint that dynamically exposes an integrated account's available API operations as callable tools (e.g., `create_a_quickbooks_invoice` or `list_all_xero_contacts`), complete with JSON Schema validation for the agent's inputs.**

In a highly scalable architecture, MCP tools are not hardcoded. Instead, they are dynamically generated from the integration's configuration and documentation records. When an AI agent connects to the MCP server endpoint, the server reads the target accounting platform's OpenAPI specification, filters the available endpoints based on the customer's OAuth scopes, and returns a structured tool list.

A tool only appears if it has a corresponding description and JSON Schema—this acts as both a quality gate and a curation mechanism, ensuring LLMs only see well-documented operations. Each tool includes a descriptive name, a JSON Schema for input parameters (query filters, cursors, request body fields), and auto-injected instructions.

Here is what the architecture looks like in practice:

```mermaid
graph TD
    A[AI Agent / LLM Client] -->|JSON-RPC 2.0 over HTTP| B[MCP Server Router]
    B -->|tools/list| C[Dynamic Tool Generator]
    C -->|Reads Schemas| D[(Integration Config DB)]
    B -->|tools/call| E[Unified API Proxy Layer]
    E -->|JSONata Mapping| F[Unified Accounting Schema]
    F -->|Transformed Request| G[Third-Party ERP]
    G -->|Native API| H[QuickBooks Online]<br>I[Xero]<br>J[NetSuite]
```

The execution flow between the agent and the underlying API follows a strict sequence:

```mermaid
sequenceDiagram
    participant Agent as AI Agent<br>(Claude, ChatGPT)
    participant MCP as MCP Server
    participant Auth as OAuth Token<br>Manager
    participant API as Xero / QBO API

    Agent->>MCP: tools/list (JSON-RPC)
    MCP-->>Agent: Available tools + JSON schemas
    Agent->>MCP: tools/call: list_invoices
    MCP->>Auth: Refresh token if expiring
    Auth-->>MCP: Fresh access token
    MCP->>API: GET /Invoices (Xero) or<br>GET /query?query=SELECT * FROM Invoice (QBO)
    API-->>MCP: Provider-specific response
    MCP-->>Agent: Normalized result + rate limit headers
```

### The Flat Input Namespace Problem

When an AI agent invokes an MCP tool via the `tools/call` method, it passes all arguments as a single, flat JSON object. However, accounting APIs require strict separation between URL path parameters, query strings, and JSON request bodies.

Your MCP server router must intelligently split this flat input namespace. It does this by comparing the agent's arguments against the `query_schema` and `body_schema` derived from the integration's documentation. If the agent calls `get_single_quickbooks_invoice_by_id` with `{"id": "123", "minorversion": "65"}`, the router identifies `id` as a path parameter and `minorversion` as a query parameter, constructing the correct upstream HTTP request without the agent needing to understand REST semantics.

For write operations, the architecture is the same. An agent calling `create_a_quickbooks_invoice` passes a body conforming to the tool's JSON Schema. The MCP server maps that to QuickBooks' native format, applies the stored OAuth credentials, and executes the request. For a deeper look at write-side safety patterns, see our guide on [whether AI agents can safely write data back to accounting systems](https://truto.one/blog/can-ai-agents-write-data-back-to-accounting-systems-like-quickbooks/).

## Handling the Brutal Reality of Accounting API Rate Limits

This is where most AI-agent-to-accounting integrations break in production. AI agents are chatty by nature—a single user query like "summarize my outstanding receivables" can trigger dozens of API calls across invoices, contacts, and payments. Accounting APIs were not designed for this access pattern.

Here is what you are actually up against when connecting to these systems:

| Constraint | Xero | QuickBooks Online | Brex |
|---|---|---|---|
| **Per-minute limit** | 60 calls/min per tenant | 500 calls/min per company | 1,000 calls/min per account |
| **Daily limit** | 5,000 calls/day per tenant | No daily cap | 1,000 transfers/day |
| **Concurrent requests** | 5 simultaneous | 10 simultaneous | Not documented |
| **App-wide limit** | 10,000 calls/min across all tenants | N/A | N/A |
| **Rate limit response** | HTTP 429 + `Retry-After` header | HTTP 429 | HTTP 429 |

Xero's limits are particularly punishing for agent workflows. At 60 calls per minute and 5,000 per day, syncing a single invoice with line items, contacts, and payments can consume 6 API calls. That means you can sync roughly 833 invoices per day before you are locked out. An AI agent that is naively fetching context for a natural-language financial query will blow through that minute limit in seconds.

QuickBooks is more generous on throughput at 500 requests per minute, but the 10-concurrent-request ceiling means a multi-tenant application serving several companies simultaneously needs careful request scheduling.

Brex sits at the more permissive end of the spectrum at 1,000 requests per minute per account. That headroom is deceptive, though. An AI agent performing bulk expense categorization - iterating through hundreds of expenses, fetching receipt data, and writing back categories - can burn through that budget fast, especially when multiple agents or background syncs share the same client credentials. Brex also enforces separate daily caps on financial operations: 1,000 transfers and 100 international wires per 24 hours.

And here is the part that matters for your architecture: **the rate limit response formats are completely different between providers.** Xero returns `X-MinLimit-Remaining` and `X-DayLimit-Remaining` custom headers. QuickBooks returns a bare 429 with no standardized remaining-count headers. Brex also returns a bare 429 without remaining-count headers. Every other accounting API (NetSuite, Zoho Books, FreshBooks) has its own convention.

### The Anti-Pattern: Absorbing Rate Limits in the Infrastructure

Many engineering teams make the mistake of implementing automatic retries with exponential backoff directly in their API proxy layer. When the proxy hits a 429, it holds the HTTP connection open, waits 30 seconds, and tries again.

**Do not do this with AI agents.**

LLMs operate on strict timeout windows. If your infrastructure holds a connection open for 45 seconds while silently retrying a Xero rate limit, the agent will timeout, assume the tool call failed, and likely hallucinate a response to the user.

### The Correct Pattern: Normalization and Agent-Side Backoff

A scalable architecture takes a different, highly opinionated approach: **The infrastructure does NOT retry, throttle, or apply backoff on rate limit errors.**

When an upstream API returns an HTTP 429, the proxy layer passes that error directly back to the caller. There is no automatic retry, no backoff, no absorption of the error. The agent or calling application receives the 429 and is responsible for deciding what to do.

What the proxy layer *does* provide is **standardized rate limit information** on every response—successful or not. Regardless of whether the upstream is Xero or QuickBooks, the platform normalizes the provider-specific headers into three consistent response headers based on the IETF RateLimit header fields specification:

*   `ratelimit-limit`: The maximum number of requests permitted in the current window.
*   `ratelimit-remaining`: The number of requests remaining in the current window.
*   `ratelimit-reset`: The number of seconds until the rate limit window resets.

By passing these normalized headers back through the MCP `isError: true` response, the AI agent's reasoning loop can natively pause its own execution. Your agent's retry logic can be entirely provider-agnostic:

```python
import time

def call_with_rate_awareness(client, endpoint, params):
    response = client.request(endpoint, params)

    remaining = int(response.headers.get("ratelimit-remaining", 100))
    reset_seconds = int(response.headers.get("ratelimit-reset", 60))

    if response.status_code == 429:
        wait_time = reset_seconds + 1  # Add jitter in production
        time.sleep(wait_time)
        return call_with_rate_awareness(client, endpoint, params)

    if remaining < 5:
        # Proactively slow down before hitting the wall
        time.sleep(reset_seconds / max(remaining, 1))

    return response
```

The same code works whether the underlying provider is Xero (with its 60/min limit) or QuickBooks (with its 500/min limit). The agent reads the same headers either way. This is the kind of plumbing that saves you from writing per-provider rate limit handling code that inevitably falls behind when providers change their policies.

## Solving OAuth Token Management and Concurrency

Giving an AI agent access to an accounting platform requires maintaining a persistent connection via OAuth 2.0. Both Xero and QuickBooks issue short-lived access tokens alongside a refresh token. Xero tokens expire after 30 minutes. QuickBooks tokens expire after 60 minutes.

Managing this lifecycle in a traditional web app is straightforward: when a user clicks a button, you check the token expiry and refresh if necessary. But AI agents operate asynchronously and concurrently. A single agent might spin up five parallel threads to query invoices, contacts, accounts, tax rates, and payments simultaneously.

If your infrastructure attempts to refresh the OAuth token for all five requests at the exact same millisecond, you will trigger a race condition. Two concurrent agent tool calls both check the token, both see it is expired, and both attempt a refresh. One succeeds. The other uses a refresh token that has now been invalidated by the first call (most OAuth providers invalidate old refresh tokens on rotation). Result: the second call fails, the integrated account enters a broken state, and your customer has to re-authenticate manually.

### The Durable Lock Pattern

To solve this, your infrastructure must implement a distributed mutex or durable lock for token refreshes per integrated account. A robust [OAuth token refresh architecture](https://truto.one/blog/oauth-at-scale-the-architecture-of-reliable-token-refreshes/) operates on two parallel tracks:

1.  **Proactive Refresh:** A background scheduler monitors all integrated accounts. Approximately 60 to 180 seconds before an access token expires, the scheduler acquires a lock and exchanges the refresh token. This ensures the token is almost always fresh when an API call arrives, imposing no latency penalty on the agent.
2.  **On-Demand Refresh with Locking:** If an agent fires a request and the token is within 30 seconds of expiration (e.g., after a long idle period), the API proxy intercepts the request. It attempts to acquire the per-account lock. 

If Thread A acquires the lock, it performs the HTTP request to QuickBooks to get the new token. Threads B, C, D, and E will see that the lock is held and simply `await` the completion of Thread A's operation. Once Thread A writes the new token to the relational database, the lock is released, and all five threads proceed using the fresh token. No duplicate refresh attempts occur.

If a refresh fails entirely—because the user revoked access or the provider is down—the integrated account is marked as requiring re-authentication, and a webhook fires so your application can notify the customer.

## Unified Accounting Models: One Schema for Xero and QuickBooks

The final piece of the architecture is schema normalization. If you simply expose raw Xero and QuickBooks endpoints via an MCP server, your AI agent still has to understand the proprietary data models of each platform. 

Xero and QuickBooks represent the same financial concepts with different field names, different nesting structures, and different enumeration values. An invoice in Xero has `Contact.ContactID`; in QuickBooks it is `CustomerRef.value`. Xero uses "AUTHORISED" as an invoice status; QuickBooks uses a numeric `Balance` field where zero implies paid. Xero calls them `Contacts` while QuickBooks calls them `Customers` and `Vendors`.

If your AI agent is generating tool calls from natural language, it cannot learn two sets of field names for the same operation. Your prompt engineering becomes polluted with platform-specific instructions. The solution is a **Unified Accounting API** layer sitting between the MCP server and the third-party endpoints. This layer abstracts away provider-specific nuances into a single, standardized data model.

### Declarative JSONata Mapping

Instead of writing integration-specific code (e.g., `if (provider === 'xero') { ... }`), scalable architectures use declarative JSONata expressions to map payloads. JSONata is a Turing-complete query and transformation language for JSON.

When an agent requests a list of unified `Contacts`, the mapping engine executes a JSONata expression against the raw upstream response. 

For QuickBooks Online, the mapping expression might look like this:

```jsonata
response.{
  "id": $string(Id),
  "name": DisplayName,
  "email": PrimaryEmailAddr.Address,
  "phone": PrimaryPhone.FreeFormNumber,
  "status": Active ? "active" : "inactive",
  "created_at": MetaData.CreateTime
}
```

For Xero, the expression for the exact same unified output looks entirely different:

```jsonata
response.{
  "id": ContactID,
  "name": Name,
  "email": EmailAddress,
  "phone": Phones[PhoneType='DEFAULT'].PhoneNumber,
  "status": ContactStatus = "ACTIVE" ? "active" : "inactive",
  "created_at": UpdatedDateUTC
}
```

The AI agent only ever sees the unified output: `id`, `name`, `email`, `phone`, `status`, `created_at`. It can execute a write operation back to the accounting system using the exact same unified schema, and the infrastructure will run a reverse JSONata mapping to translate the request body into the format expected by Xero or QuickBooks.

Because the unified schema is consistent across providers, the MCP tool definitions are identical regardless of whether the connected account is Xero or QuickBooks. The LLM sees `list_all_contacts` with one JSON Schema. Fewer tools means smaller context windows, which means better model performance and lower token costs. For a deeper comparison of unified accounting APIs, see our [2026 guide to the best unified accounting API](https://truto.one/blog/the-best-unified-accounting-api-for-b2b-saas-and-ai-agents-2026/).

Critically, the original provider-specific response is always preserved as `remote_data` on the unified response. If an agent needs a Xero-specific field that isn't in the unified schema—like tracking categories or branding themes—it is right there. The unified model doesn't strip data; it normalizes access to the most common fields while keeping everything else available.

### Pagination and Cursor Instructions

Accounting APIs return massive datasets, making pagination critical. Xero uses page numbers, while QuickBooks uses SQL-like offset queries (`STARTPOSITION`). 

The unified API normalizes these into standard `next_cursor` strings. However, LLMs are notorious for trying to parse, decode, or alter pagination cursors. To prevent this, the MCP server dynamically injects strict prompt instructions into the tool's `query_schema` description:

> [!TIP]
> **LLM Prompt Engineering Tip:** In your dynamically generated tool schema, explicitly define the cursor behavior: *"The cursor to fetch the next set of records. Always send back exactly the cursor value you received (nextCursor) without decoding, modifying, or parsing it."*

> [!WARNING]
> **Xero policy change (March 2, 2026):** Xero's updated Developer Terms now explicitly prohibit using API data to train AI/ML models. If your agent architecture involves fine-tuning on customer financial data pulled from Xero, this is a compliance blocker. Read-time inference (where an agent queries data, reasons over it, and takes action) is a different pattern and is not affected—but you should always check with your legal team.

## Connecting AI Agents to Brex Expense Data via MCP

The architectural patterns above for Xero and QuickBooks apply directly to Brex, but with important differences. Brex is not a traditional double-entry accounting system - it is a unified spend management platform covering corporate cards, expense management, reimbursements, travel, and bill pay. The API surface reflects this: instead of invoices and chart-of-accounts entries, your AI agent works with expenses, card transactions, budgets, spend limits, and receipt uploads.

### Why MCP Servers for Brex

Brex is where spend data lives for thousands of startups and enterprises. Finance teams want AI agents that can categorize expenses automatically, match receipts to card transactions, flag policy violations, and generate spend reports - all without manual dashboard work.

Without an MCP server, you are back to the N×M problem. Every AI model that needs Brex access requires its own integration code. Your agent framework needs custom Brex tool definitions. And when Brex updates its API, every integration path breaks.

An MCP server for Brex solves this: you define the tools once, and any MCP-compatible client discovers and executes them. The same `list_all_brex_expenses` tool works whether the caller is Claude Desktop, ChatGPT, or a custom ReAct agent. Because Brex publishes OpenAPI specs for all its APIs (Expenses, Transactions, Payments, Team, Budgets), the MCP server can dynamically generate tools from those specs using the same documentation-driven approach that works for Xero and QuickBooks.

### End-to-End Flow: Agent to Brex via MCP Server

Here is the complete request lifecycle when an AI agent queries Brex expense data through an MCP server backed by a unified API proxy:

```mermaid
sequenceDiagram
    participant Agent as AI Agent<br>(Claude, ChatGPT)
    participant MCP as MCP Server
    participant Lock as Token Refresh<br>Lock
    participant Proxy as Unified API<br>Proxy Layer
    participant Brex as Brex API<br>(api.brex.com)

    Agent->>MCP: tools/list (JSON-RPC)
    MCP-->>Agent: list_all_brex_expenses,<br>update_a_brex_card_expense_by_id,<br>brex_card_expenses_receipt_upload
    Agent->>MCP: tools/call: list_all_brex_expenses<br>{"purchase_date_after": "2026-03-01"}
    MCP->>Lock: Check token expiry, acquire lock
    Lock->>Brex: POST /oauth2/token (refresh grant)
    Brex-->>Lock: New access_token (1h TTL)<br>+ rotated refresh_token
    Lock-->>MCP: Fresh access token
    MCP->>Proxy: Forward request with Bearer token
    Proxy->>Brex: GET /v1/expenses?purchase_date_after=...
    Brex-->>Proxy: {items: [...], next_cursor: "abc"}
    Proxy-->>MCP: Normalized response +<br>ratelimit-remaining header
    MCP-->>Agent: {result: [...], next_cursor: "abc"}
```

The proxy layer handles the translation between the agent's flat argument namespace and Brex's actual REST endpoints. The agent never knows it is talking to Brex - it just calls tools and gets structured JSON back.

### MCP Tool Schema Examples for Brex Expense Operations

Here is what dynamically generated MCP tool definitions look like for Brex expense operations. These are the JSON objects returned in a `tools/list` response:

**Read: List all expenses**

```json
{
  "name": "list_all_brex_expenses",
  "description": "List all expenses from the connected Brex account. Returns card expenses, reimbursements, and other expense types with amounts, merchants, categories, and receipt status.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "expense_type": {
        "type": "string",
        "description": "Filter by expense type (e.g., CARD, REIMBURSEMENT)"
      },
      "purchase_date_after": {
        "type": "string",
        "description": "ISO 8601 date. Only return expenses purchased on or after this date."
      },
      "limit": {
        "type": "string",
        "description": "The number of records to fetch"
      },
      "next_cursor": {
        "type": "string",
        "description": "The cursor to fetch the next set of records. Always send back exactly the cursor value you received (nextCursor) without decoding, modifying, or parsing it."
      }
    }
  }
}
```

**Write: Update a card expense**

```json
{
  "name": "update_a_brex_card_expense_by_id",
  "description": "Update a card expense in the connected Brex account by its expense ID. Use this to categorize an expense, add a memo, or update metadata. Admin and bookkeeper roles can update any expense; regular users can only update their own.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "expense_id": {
        "type": "string",
        "description": "The ID of the card expense to update. Required."
      },
      "memo": {
        "type": "string",
        "description": "A memo or note for the expense"
      },
      "category": {
        "type": "string",
        "description": "The category of the expense"
      }
    },
    "required": ["expense_id"]
  }
}
```

**Custom method: Upload a receipt**

```json
{
  "name": "brex_card_expenses_receipt_upload",
  "description": "Upload a receipt for a specific Brex card expense. The receipt will be attached to the expense for compliance and audit purposes.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "expense_id": {
        "type": "string",
        "description": "The ID of the card expense to attach the receipt to. Required."
      },
      "receipt_name": {
        "type": "string",
        "description": "The filename of the receipt image"
      },
      "receipt_url": {
        "type": "string",
        "description": "A URL where the receipt image can be downloaded"
      }
    },
    "required": ["expense_id"]
  }
}
```

The tool naming convention follows the same pattern as other integrations: `list_all_brex_expenses`, `get_single_brex_expense_by_id`, `update_a_brex_card_expense_by_id`. Custom methods like receipt upload get a descriptive compound name. This consistency means an agent that already knows how to call `list_all_xero_contacts` can immediately work with Brex tools without any prompt changes.

Here is a code snippet demonstrating how an agent invokes these tools and handles the response:

```python
import json

def invoke_brex_tool(mcp_client, tool_name, arguments):
    response = mcp_client.call_tool(tool_name, arguments)

    if response.get("isError"):
        error = json.loads(response["content"][0]["text"])
        if error.get("status_code") == 429:
            reset = int(error.get("headers", {}).get("ratelimit-reset", 60))
            return {"retry_after": reset, "error": "rate_limited"}
        raise Exception(f"Tool call failed: {error}")

    return json.loads(response["content"][0]["text"])

# List recent Brex expenses
expenses = invoke_brex_tool(client, "list_all_brex_expenses", {
    "purchase_date_after": "2026-03-01",
    "limit": "50"
})

# Categorize an expense
invoke_brex_tool(client, "update_a_brex_card_expense_by_id", {
    "expense_id": expenses["result"][0]["id"],
    "memo": "Q1 team offsite - meals",
    "category": "Meals & Entertainment"
})
```

### OAuth Concurrency and Durable Locks for Brex

Brex supports two authentication methods, and the one you need depends on your use case. For multi-tenant SaaS applications connecting to your customers' Brex accounts, you must use OAuth 2.0 as a registered Brex partner. For internal tools connecting to your own Brex account, you can use API tokens generated from the Brex dashboard.

The OAuth path is where concurrency gets dangerous. Brex partner OAuth access tokens expire after 3,600 seconds (1 hour) - more generous than Xero's 30-minute window, but still short enough that any long-running agent session will need at least one refresh. The critical detail: Brex enforces refresh token rotation on every exchange. Each time you trade a refresh token for a new access token, Brex returns a new refresh token and invalidates the old one.

This makes the durable lock pattern described earlier even more important for Brex than for providers with more lenient token policies. If two concurrent agent threads both try to refresh using the same refresh token, the first one succeeds and gets a fresh token pair. The second thread's refresh token is now invalid - Brex has already rotated it. Without a lock, that second thread's failure cascades: the integrated account enters a broken state, and your customer has to re-authorize through the OAuth flow.

The lock pattern for Brex works identically to Xero and QuickBooks:

1. **Proactive refresh:** A background scheduler refreshes Brex tokens approximately 60 to 180 seconds before the 1-hour expiry. The scheduler acquires the per-account lock, exchanges the refresh token, stores both the new access token and the new refresh token, then releases the lock.
2. **On-demand refresh with locking:** If an agent request arrives and the token is near expiry, the proxy acquires the lock. Concurrent callers wait on the lock rather than attempting their own refresh. Once the lock holder completes the exchange, all waiting threads proceed with the fresh token.

For Brex API tokens (the non-OAuth path), the concurrency problem is simpler. These tokens do not expire on a fixed schedule - they remain valid as long as they are used at least once every 30 days. No refresh flow is needed, so no lock is needed. But if your MCP server supports both OAuth-connected and API-token-connected Brex accounts, the token management layer must handle both paths.

### Rate-Limit Handling for Brex

Brex allows 1,000 requests per 60 seconds per client ID and Brex account. That sounds generous, but consider a bulk expense categorization workflow: an agent lists 200 expenses, fetches details on each one, looks up the associated card transaction for context, then writes back a category. That is 600+ API calls for a single user request.

Brex returns a bare HTTP 429 when you exceed the limit - no `Retry-After` header, no remaining-count headers. This makes client-side rate tracking important. Since Brex does not tell you how many requests remain in the current window, your proxy layer should maintain a sliding window counter per client-ID-and-account pair. When the counter approaches 1,000, proactively slow down before hitting the wall.

The normalized rate limit headers (`ratelimit-limit`, `ratelimit-remaining`, `ratelimit-reset`) that the proxy layer returns to the agent work the same way as for Xero and QuickBooks. Even though Brex does not natively provide remaining-count headers, the proxy can compute them from its own request counter and pass them through to the agent. The agent's backoff logic remains provider-agnostic.

One gotcha specific to Brex: the 1,000-transfer-per-day limit and the 100-international-wire-per-day limit are separate from the general API rate limit. These are financial operation caps, not API throughput caps. If your agent performs payment operations through Brex, it needs to track these daily budgets independently.

### Idempotency for Brex Write Operations

Brex requires an `Idempotency-Key` header on certain write endpoints - particularly `Create transfer` and `Create card`. This is a safety mechanism: if a network failure causes the agent to retry a payment creation, the idempotency key ensures Brex processes it only once.

Your MCP server should auto-generate and attach idempotency keys for every Brex write operation. When the agent calls `tools/call` for a write tool, the proxy layer generates a UUID, attaches it as the `Idempotency-Key` header, and stores the mapping between the tool call ID and the idempotency key. If the agent retries the same logical operation (same tool, same arguments), the proxy reuses the stored key rather than generating a new one.

This is especially important for AI agents because LLMs will sometimes retry tool calls if they do not receive a response within their timeout window. Without idempotency keys, a retry could create duplicate payments or duplicate cards.

## Testing, Monitoring, and Observability

Shipping an MCP server for financial APIs without proper observability is asking for trouble. Financial data has zero tolerance for silent failures.

**Testing against rate limits.** Before going to production, load-test your MCP server against realistic agent workloads. Simulate a bulk expense categorization workflow (list, fetch details, update) and verify that your sliding window counter correctly throttles requests before hitting the provider's limit. Brex provides a staging environment at `api-staging.brex.com`, but note that it is not a sandbox and does not work with customer tokens - you will need partner credentials for meaningful testing.

**Monitoring token refresh health.** Track the success rate of OAuth token refreshes per integrated account. A spike in refresh failures typically means a customer revoked access or Brex changed something on their end. Alert on any account that enters a needs-reauth state so your support team can proactively reach out.

**Tracking 429 rates per tenant.** Aggregate rate limit errors by provider and tenant. If a specific Brex account consistently hits 429s, it likely means an agent workflow is too aggressive for that account's API budget. Surface this data in a dashboard so your team can tune the agent's batch sizes or add deliberate pauses.

**Logging MCP tool invocations.** Every `tools/call` request should be logged with the tool name, the integrated account ID, a request ID for traceability, latency, and the HTTP status code from the upstream provider. This lets you trace a single agent action from the LLM's tool call all the way through to the Brex API response. When a customer reports "the agent said my expense was categorized but it wasn't," you need that audit trail.

**Alerting on schema drift.** Brex, Xero, and QuickBooks all evolve their APIs over time. Monitor for unexpected response shapes - new fields, changed enum values, deprecated endpoints. Brex explicitly recommends that integrations handle unknown enum types and new response fields gracefully. If your JSONata mappings start producing `null` values for previously populated fields, that is a signal to update your integration configuration.

## Strategic Wrap-Up and Next Steps

Connecting AI agents to Xero and QuickBooks requires far more than just wrapping REST endpoints in an MCP server. You are building distributed systems infrastructure. 

To achieve production-grade reliability, your architecture must solve three hard problems: rate limit awareness, OAuth lifecycle management, and schema normalization. Specifically, your system must:

1.  **Eliminate custom code** by dynamically generating MCP tools from API schemas.
2.  **Normalize rate limit headers** and pass 429 errors back to the agent so it can handle its own backoff logic using standard IETF headers.
3.  **Implement a durable lock** to prevent OAuth token refresh race conditions during concurrent agent tool calls.
4.  **Deploy a unified accounting schema** using declarative transformations so your agents only need one set of prompts to interact with any ERP.

If you are evaluating this stack, here is the honest trade-off matrix:

| Approach | Time to first integration | Multi-provider support | Maintenance burden |
|---|---|---|---|
| **Build direct connectors** | 2-4 weeks per provider | Each provider is a separate codebase | High (OAuth, rate limits, schema drift) |
| **Use a unified API + MCP** | Days | One schema, one tool set | Low (managed by the platform) |
| **Single-provider MCP server** | 1-2 weeks | Only covers one provider | Medium (you own the OAuth and rate limit logic) |

The math gets worse as you add providers. QuickBooks and Xero cover a large share of the SMB market, but enterprise deals will ask for NetSuite, Sage Intacct, and Dynamics 365. Each new direct connector multiplies your maintenance surface.

Start by reading the [QuickBooks Online API integration guide](https://truto.one/blog/how-to-integrate-with-the-quickbooks-online-api-2026-guide/) and the [Xero API integration guide](https://truto.one/blog/how-do-i-integrate-with-the-xero-api-a-guide-for-b2b-saas/) to understand the provider-specific quirks. Then evaluate whether abstracting those quirks behind a unified API and MCP server layer saves your team enough time to justify the dependency.

Building this infrastructure in-house requires months of dedicated engineering time—time that should be spent improving your agent's reasoning capabilities, not debugging QuickBooks API quirks.

> Stop wasting engineering cycles on accounting API maintenance. Use unified APIs and managed MCP servers to give your AI agents instant, reliable access to Xero, QuickBooks, Brex, and 100+ other enterprise platforms without writing per-provider code.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
