Skip to content

How to Integrate the Brex API with Your Accounting Stack: The 2026 Engineering Guide

A complete 2026 engineering guide to integrating the Brex API with accounting systems like QuickBooks, Xero, and NetSuite. Covers rate limits, ledger mapping, and unified architecture.

Uday Gajavalli Uday Gajavalli · · 20 min read
How to Integrate the Brex API with Your Accounting Stack: The 2026 Engineering Guide

If you are building a B2B SaaS product that touches corporate spend—whether that is expense management, AP automation, procurement, or financial reporting—your customers expect you to sync their Brex transactions directly into their general ledger. Over 75% of business payments are now made through digital methods rather than analog ones, according to the Association for Financial Professionals (AFP). Yet, Mastercard reports that 69% of companies still struggle to integrate their payment systems with their business software.

When a customer swipes a Brex card, that raw transaction is just the beginning. To be useful to a finance team, that swipe must be categorized, matched with a receipt, assigned to a specific department, and written into an accounting system like QuickBooks Online, Xero, or NetSuite as a reconciled expense or journal entry.

Building this data pipeline requires more than just calling a few REST endpoints. You are bridging a high-volume fintech API with rigid, double-entry accounting ledgers. This guide breaks down the architectural requirements, rate limit handling, and ledger mapping strategies necessary to reliably integrate the Brex API with any accounting platform, and explains why point-to-point integrations between spend platforms and ERPs inevitably collapse at scale.

The Architecture of a Brex-to-Accounting Integration

The core data flow between a corporate card provider and an accounting system follows a predictable pattern: ingest, normalize, map, and write. However, the bidirectional nature of this flow is where the engineering complexity lies.

flowchart LR
    A["Brex API<br>(Transactions, Expenses,<br>Team, Payments)"] --> B["Your Mapping Layer<br>(Field transforms,<br>GL account routing,<br>currency conversion)"]
    B --> C["Target ERP/Accounting<br>(QuickBooks, Xero,<br>NetSuite, Zoho Books)"]
    C -->|Chart of Accounts,<br>Tax Rates, Vendors| B

The left side of your architecture is read-heavy: you pull settled card transactions, expense reports with receipt metadata, user/department hierarchies, and payment transfers from Brex. The right side is write-heavy: you create journal entries, bills, expenses, or vendor credits in the target accounting system.

The middle layer—the mapping logic—is where 80% of the engineering pain lives. This is not a one-directional pipe. Brex's Accounting API goes beyond traditional bank-feed integrations by enabling two-way data flow between Brex and ERPs. Your application needs to listen for spend events, enrich those events, and read the chart of accounts, tax rates, and tracking categories from the accounting platform so that Brex transactions land in the correct GL accounts.

Here is how a standard webhook-driven write flow operates in practice:

sequenceDiagram
    participant Brex as Brex API
    participant SaaS as Your Application
    participant Ledger as Accounting API (QBO/NetSuite)
    
    Brex->>SaaS: Webhook: expense.created
    SaaS->>Brex: GET /v1/expenses/{id}<br>(Fetch full receipt & metadata)
    Brex-->>SaaS: JSON Expense Payload
    Note over SaaS: Normalize data<br>Map to Chart of Accounts
    SaaS->>Ledger: POST /unified/accounting/expenses<br>(Write to General Ledger)
    Ledger-->>SaaS: 201 Created (Expense ID)
    SaaS->>Brex: Update expense status (Optional)

For a deep dive into how destination systems operate, read our guide on What Are Accounting Integrations?.

Brex APIs use standard REST architecture, are defined using the OpenAPI specification, and use standard HTTP response codes and verbs. All APIs accept and return JSON and require HTTPS. To build a complete spend management or accounting sync feature, you will primarily interact with these API surfaces:

API What It Gives You Key Endpoints
Transactions API Settled card and cash transactions with amounts, merchants, and expense IDs GET /v2/transactions/card/primary, GET /v2/transactions/cash/{id}
Expenses API Expense metadata, receipt attachments, coding fields, approval status GET /v1/expenses/card, PUT /v1/expenses/card/{expense_id}
Team API Users, departments, locations, card provisioning GET /v2/users, POST /v2/cards
Payments API ACH, wire, and check transfers to vendors GET /v1/vendors, POST /v1/vendors
Accounting API Two-way ERP sync with real-time webhooks Expanded endpoints for GL mapping
Webhooks API Real-time event notifications for transactions and expenses POST /v1/webhooks

The Transactions API: Your Source of Truth

This is your source of truth for settled financial movements. The Transactions API provides the raw ledger of debits and credits on the Brex account. Because accounting systems require reconciled, settled data to close the books, this API is the foundation of your sync logic.

A critical detail the docs won't tell you upfront: Brex doesn't return pending transactions today. Transactions are not returned in real-time as they happen; they will only be returned in the API as transactions settle. This means your accounting sync will always run on a settlement delay. You cannot build a real-time dashboard off the Transactions API alone. The Webhooks API with the EXPENSE_PAYMENT_UPDATED event type helps close this gap, but settled transaction data remains the authoritative source.

The Expenses API: Adding Business Context

While transactions represent the movement of money, expenses represent the business context. The Expenses API provides the metadata: memos, category tags, linked receipts, and approval statuses. When you map corporate spend to a general ledger, you are usually merging data from the Transactions API (the amount and date) with the Expenses API (the receipt and category).

When fetching expenses, use the expand [] query parameter to hydrate nested objects in a single call instead of chasing IDs. Common expansions include merchant, location, department, budget, receipts.download_uris, and payment. Every extra round-trip you save at ingest is a round-trip you don't have to rate-limit later.

The Team API: Automating Issuance

Automating card issuance and spend limits is a core use case for corporate card APIs. You can dynamically create virtual cards with spend limits for new employees or vendors through the Team API, and change spend limits per card instantly. In an accounting context, you also use the Team API to map Brex users to specific Employees or Vendors in the target ERP.

Authentication Patterns

Brex supports two auth patterns, and the choice matters for how you design token storage and rotation:

1. User-scoped API tokens (single tenant). Best for internal tools or single-company deployments. A finance admin generates a token from the Brex dashboard, you store it encrypted, and you pass it as Authorization: Bearer <token>. Simple, but token rotation is a manual operation.

2. OAuth 2.0 (multi-tenant SaaS). Required if you are building a product that many Brex customers connect to. The flow is a standard authorization code grant: redirect the user to Brex's authorization URL with your client_id, redirect_uri, and requested scopes, then exchange the returned code for an access token and refresh token at the token endpoint. Access tokens are short-lived; refresh tokens must be exchanged before expiry.

Scope selection is where teams get burned. Request only what you need. For an accounting sync, the typical minimum scope set is transactions.card:read, transactions.cash:read, expenses:read, expenses:write (if you push GL codes back to Brex), team:read, and webhooks:write. Over-requesting scopes will trigger security review pushback from enterprise customers.

Tip

Store refresh tokens with a rotation clock, not an expiry clock. Refresh tokens should be pre-emptively rotated well before their nominal expiry. If a background sync job runs every 6 hours but your token has a 24-hour TTL, refresh at the start of the job rather than reacting to a 401. Reactive refresh is where race conditions and silent sync failures live.

If you route through Truto, both patterns are handled by the platform. Truto refreshes OAuth tokens shortly before they expire and injects the correct Authorization header on every proxied call, so your application code never touches raw credentials.

Info

Capital One Acquisition Context: Capital One completed its acquisition of Brex in April 2026 in a combination of stock and cash transactions. The Brex API continues to operate at platform.brexapis.com as of this writing, but engineering teams should monitor the developer changelog for any infrastructure or endpoint migrations as the integration progresses.

Rate Limiting and Retry-After Strategies

High-volume financial APIs protect their infrastructure aggressively. Like most public APIs, Brex uses rate limiting to prevent buggy clients or denial-of-service attacks from affecting availability for others.

Brex rate limits are strictly enforced per Client ID and per Brex account. Brex allows up to 1,000 requests in 60 seconds, up to 1,000 transfers in 24 hours, up to 100 international wires in 24 hours, and up to 5,000 cards created in 24 hours. Exceeding a rate limit will result in an HTTP 429 (Too Many Requests) response.

Brex suggests throttling traffic more broadly per Client ID rather than for individual requests. A token bucket algorithm implemented per Client ID helps mitigate the effect of rate limits on your application.

A Concrete Retry Algorithm

Don't roll your own retry logic ad-hoc in each caller. Centralize it in a single HTTP wrapper that implements exponential backoff with full jitter, honors Retry-After, and gives up after a bounded number of attempts. The algorithm below is the pattern that survives production traffic:

import random
import time
import requests
 
RETRYABLE_STATUSES = {408, 425, 429, 500, 502, 503, 504}
 
def brex_request(method, url, *, headers=None, params=None, json_body=None,
                 max_attempts=6, base_delay=1.0, cap=60.0):
    """
    Exponential backoff with full jitter, respecting Retry-After.
    Delay = random.uniform(0, min(cap, base_delay * 2 ** attempt))
    """
    last_resp = None
    for attempt in range(max_attempts):
        last_resp = requests.request(
            method, url, headers=headers, params=params, json=json_body,
            timeout=30,
        )
 
        if last_resp.status_code not in RETRYABLE_STATUSES:
            return last_resp
 
        # Prefer server guidance when present.
        retry_after = last_resp.headers.get("Retry-After")
        if retry_after is not None:
            try:
                delay = float(retry_after)
            except ValueError:
                # HTTP-date form; fall back to backoff.
                delay = min(cap, base_delay * (2 ** attempt))
        else:
            delay = random.uniform(0, min(cap, base_delay * (2 ** attempt)))
 
        time.sleep(delay)
 
    return last_resp

A few things to notice: full jitter (not "equal jitter" or fixed backoff) is what prevents thundering-herd retries when many workers hit a 429 at the same instant. Never retry non-idempotent writes (POST to create a card, POST to initiate a transfer) without an idempotency key - a naive retry can double-book a payment. And cap your total retry budget. If a request has been failing for 5 minutes, escalate it to a dead-letter queue rather than blocking the worker.

Handling this across multiple third-party APIs quickly becomes a distributed systems problem because every provider returns rate limit information differently. If you route Brex API calls through Truto's Proxy API, the platform normalizes upstream rate limit information into standardized headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) per the IETF draft spec.

Note: Truto does not retry, throttle, or absorb 429 errors on your behalf. When Brex returns an HTTP 429, Truto passes that error directly to the caller. Your application is responsible for implementing backoff logic. This transparency gives you an accurate signal without hiding upstream behavior. For architectural patterns, review our guide on Best Practices for Handling API Rate Limits.

Pagination Normalization and Cursor Strategies

Brex uses a cursor-based pagination approach. All "list" API endpoints support pagination with two parameters: limit (number of items per call) and cursor (the starting point for the set of items). The default limit is 100. It can be set up to 1000, and values greater than 1000 return a 400 Bad Request error.

Cursor pagination is safer than offset pagination for financial data because inserts on the server side don't shift page boundaries. But there are three practical concerns worth designing for up front:

1. Cursor durability. Persist the last successful cursor to a database, not just to memory. If your worker crashes mid-sync, you resume from the last checkpoint, not from the start. A sync_state row per (integrated_account_id, resource) tuple is enough.

2. Delta vs. full sync. Combine cursors with posted_at_start / posted_at_end filters on the Transactions API to run incremental deltas after the initial backfill. Store the max posted_at from each successful page as your low-water mark for the next run.

3. Consistent normalization across providers. Brex uses next_cursor, QuickBooks uses startPosition with maxResults, Xero uses a page number, NetSuite uses offset/limit. If you are calling multiple accounting systems, wrap all of them in a single paginate(iterator) helper so the calling code doesn't care.

Here is a practical pagination loop in Python that combines cursor persistence with the retry wrapper above:

def fetch_all_transactions(token, account_id, cursor_store, limit=500):
    url = "https://platform.brexapis.com/v2/transactions/card/primary"
    headers = {"Authorization": f"Bearer {token}"}
    cursor = cursor_store.get(account_id)
 
    while True:
        params = {"limit": limit}
        if cursor:
            params["cursor"] = cursor
 
        resp = brex_request("GET", url, headers=headers, params=params)
        resp.raise_for_status()
        data = resp.json()
 
        yield from data.get("items", [])
 
        cursor = data.get("next_cursor")
        # Checkpoint after each page so a crash resumes here.
        cursor_store.set(account_id, cursor)
 
        if not cursor:
            break

Mapping Corporate Spend to the General Ledger

The most complex engineering challenge in this integration is not the HTTP request—it is the accounting logic. A Brex card transaction is a flat financial event: amount, merchant, date, card holder, maybe an expense category. A general ledger entry is a structured, double-entry record—similar to the complexities we covered in our Stripe accounting integration guide—that must debit and credit specific accounts, apply the correct tax treatment, reference the right vendor, and respect the target system's strict validation rules.

When you push an expense into an accounting system, your mapping layer must resolve several relational dependencies:

1. GL Account Resolution (The Chart of Accounts) Every Brex expense needs to map to a specific account in the customer's Chart of Accounts. A "Software Subscription" expense in Brex might map to account 6200 - Software & Subscriptions in one customer's QuickBooks instance and 7100 - IT Expenses in another. Your application needs to query the accounting system's Accounts endpoint, cache the active expense accounts, and provide a configurable mapping UI for the user.

2. Vendors and Contacts Accounting systems require a vendor for every expense. Brex merchant names rarely match vendor records in accounting systems exactly. If an employee buys a flight on Delta Airlines, "DELTA AIR" from the Brex card transaction needs to resolve to "Delta Airlines" in the accounting system's Contacts directory. If it exists, you link the ID. If it does not, you must create it dynamically before writing the expense. Fuzzy matching, alias tables, or manual mapping UIs are table-stakes features here.

3. Tracking Categories and Custom Segments Enterprise customers using Xero's tracking categories, QuickBooks classes, or NetSuite's custom segments expect card spend to flow into the correct dimensional buckets. A Brex transaction might need to be tagged to "Department: Engineering" and "Location: New York". Your integration must dynamically fetch these categories and append them to the payload.

Warning

NetSuite OneWorld Edge Case: If your customers use NetSuite OneWorld with multi-subsidiary and multi-currency enabled, your integration must dynamically detect these features and adjust query construction accordingly—including or excluding JOINs for currency and subsidiary tables based on the customer's NetSuite edition. Failure to handle these dependencies results in rejected API calls, out-of-balance ledgers, and extremely unhappy finance teams.

Multi-Currency and Tax Handling Guidance

Multi-currency and tax logic is where naive integrations silently corrupt customer books. The failure mode is not a 500 error; it is a ledger that balances in the API response but is off by a few cents per transaction because you rounded before the ledger did.

FX Rate Sourcing and Application

Brex settles card transactions in the account's base currency and exposes both the original transaction currency and the settled amount. When you write to the ledger, three fields matter:

  • Original amount and currency - what the merchant charged (e.g., 89.50 EUR).
  • Settled amount and currency - what Brex booked to the account (e.g., 96.42 USD).
  • Implied FX rate - settled_amount / original_amount, computed to at least 6 decimal places.

The rule of thumb: always send the ledger the original currency and the FX rate, not just the converted amount. Most accounting systems (Xero, QBO, NetSuite OneWorld) will compute their own converted amount from the rate. If you send only the pre-converted amount, you lose the audit trail and often trigger "exchange gain/loss" reconciliation entries the finance team has to clean up manually.

Rounding Discipline

Use decimal arithmetic, never floats. Round at the same precision the target ledger uses (typically 2 decimal places for the base currency, 6 for FX rates). Round once, at the boundary of writing to the ledger. Every intermediate calculation stays at full precision.

from decimal import Decimal, ROUND_HALF_UP
 
def to_ledger_amount(amount_minor_units, currency_exponent=2):
    # Brex returns amounts in minor units (cents).
    quantum = Decimal(10) ** -currency_exponent
    return (Decimal(amount_minor_units) / (Decimal(10) ** currency_exponent)).quantize(
        quantum, rounding=ROUND_HALF_UP
    )

Tax Rate Resolution

Different jurisdictions require different tax treatments. A Brex transaction in the UK might need VAT applied; the same merchant type in a US state might be tax-exempt. Your mapping layer must pull the customer's configured TaxRates from the accounting system and apply the correct one based on transaction metadata to ensure compliance.

The pattern that works:

  1. On connection, fetch the full tax rate list from the accounting system and cache it with a TTL of 24 hours (rates change infrequently).
  2. For each Brex expense, resolve the tax rate using a priority chain: explicit per-expense overrideper-vendor defaultper-GL-account defaultcustomer-wide fallback.
  3. Persist the resolution decision on the created ledger record so audit reviews can trace why a specific rate was applied.

NetSuite is the outlier here: tax rate detail requires a legacy SOAP call because the modern query surface doesn't expose the full record structure. If you are writing directly to NetSuite, budget engineering time for SOAP envelope construction and HMAC-SHA256 signing. If you route through a unified layer, the SOAP fallback is handled behind the same JSON endpoint.

Why Point-to-Point Integrations Fail at Scale

Building a direct, point-to-point integration between Brex and QuickBooks Online is a manageable project for a senior engineer. One team, one sprint, one mapping layer. The architecture breaks down when your sales team signs a mid-market customer using Xero, and an enterprise customer running NetSuite, Sage Intacct, or Zoho Books.

If you support 3 spend platforms and 5 accounting systems, you are maintaining 15 integration paths. Every accounting platform has a completely different API philosophy:

  • QuickBooks Online uses a quirky REST-ish API with strict SQL-like querying and aggressive pagination limits that throttle at the company level.
  • Xero requires complex OAuth2 scopes, uses a sliding window rate limit per app, and handles multi-currency entirely differently than QBO.
  • NetSuite requires orchestrating across SuiteTalk REST, SuiteScript, and legacy SOAP endpoints just to fetch basic tax rates and custom fields.

If you build point-to-point, your codebase will soon be littered with integration-specific conditional logic. You will maintain separate database tables for QuickBooks tokens, NetSuite credentials, and Xero tenant IDs.

The real cost is not the initial build; it is the maintenance. Every time Brex or an ERP ships a new API version, deprecates an endpoint, or changes a field name, your engineering team must drop feature work to update the integration. This technical debt scales linearly, eventually consuming a massive percentage of your engineering capacity.

Architectural Patterns: Unified API Layers and MCP Servers

There are two architectural patterns worth understanding before you write a line of Brex integration code: the unified API layer and the MCP server. They solve overlapping problems for different consumers.

Unified API Layer

A unified API layer sits between your application and every downstream provider (Brex on one side, QBO/Xero/NetSuite on the other). Your app speaks one schema; the layer translates. This is the pattern most B2B SaaS teams should default to, because your consumers are your own backend services that already know how to speak REST.

MCP Server (Model Context Protocol)

MCP is the protocol AI agents use to discover and call external tools. If part of your product is an AI agent that answers "how much did engineering spend on SaaS last month?" or automatically categorizes uncoded Brex expenses at month-end, exposing your integration surface as an MCP server lets any MCP-compatible agent (Claude, cursor-style IDE agents, internal LLM apps) call Brex and the accounting system with strongly typed tool definitions.

The important insight: MCP and a unified API layer are not competing patterns - they compose. The MCP server exposes tools like list_brex_expenses, create_ledger_expense, resolve_vendor. Behind those tools, the unified API layer handles the actual provider calls, auth, rate limits, and schema translation. The agent never sees the difference between QBO and NetSuite; it just calls create_ledger_expense and the layer routes.

flowchart LR
    Agent["AI Agent<br>(Claude, internal LLM)"] -->|MCP tool call| MCP["MCP Server"]
    App["Your Backend<br>Service"] -->|REST| Unified["Unified API Layer"]
    MCP -->|Same tools| Unified
    Unified -->|Provider-specific| Brex["Brex API"]
    Unified -->|Provider-specific| QBO["QuickBooks"]
    Unified -->|Provider-specific| Xero["Xero"]
    Unified -->|Provider-specific| NS["NetSuite"]

This pattern also makes credential management sane. Both the deterministic backend service and the AI agent authenticate against the same layer with the same integrated_account_id, so you don't end up with two copies of OAuth token storage.

Using Truto to Sync Brex with Any Accounting Platform

To solve the integration scaling problem, engineering teams use unified APIs to abstract away provider-specific differences. Instead of writing code for QuickBooks, Xero, and NetSuite individually, you integrate once against a single standardized schema.

Truto provides a zero-code architecture that makes this process highly reliable. The platform handles 100+ third-party integrations without a single line of integration-specific code in its runtime logic. Integration behavior is defined entirely as declarative JSON configuration, and data mapping is handled via JSONata expressions.

Here is what this looks like in practice for a Brex-to-accounting sync:

Reading from Brex via the Proxy API Truto's Proxy API gives you direct, unmapped access to any Brex endpoint. You send a request to /proxy/transactions/card/primary, and Truto handles credential injection, token refresh, and rate limit header normalization—then returns the raw Brex response. This is useful for accessing highly specific, proprietary endpoints like a niche virtual card issuance feature that doesn't fit a generic schema.

Writing to Accounting via the Unified Accounting API On the other side, Truto's Unified Accounting API normalizes the chaotic schemas of QBO, Xero, and NetSuite. You push a standardized Expense or JournalEntry JSON payload to Truto, and the platform translates it into the provider-specific format, handling the complex nested arrays and custom field mappings automatically.

sequenceDiagram
    participant App as Your App
    participant Proxy as Truto Proxy API
    participant Brex as Brex API
    participant Unified as Truto Unified<br>Accounting API
    participant ERP as QuickBooks / Xero /<br>NetSuite

    App->>Proxy: GET /proxy/transactions/card/primary
    Proxy->>Brex: GET /v2/transactions/card/primary
    Brex-->>Proxy: Raw transaction data
    Proxy-->>App: Brex response + normalized rate limit headers
    App->>App: Transform & map to unified schema
    App->>Unified: POST /unified/accounting/expenses
    Unified->>ERP: Provider-specific API call
    ERP-->>Unified: Response
    Unified-->>App: Unified response

When building with Truto, you leverage several architectural advantages:

  • Pass-Through Architecture (Zero Data Retention): Financial data is highly sensitive. Truto operates as a pure pass-through proxy. It does not store your customers' sensitive financial or transaction data. The platform processes the payload in memory, executes the JSONata transformations, and immediately forwards the request to the destination. This is critical for SOC 2 and enterprise security reviews.
  • Automated Token Management: OAuth token lifecycle management is notoriously difficult in distributed systems. Truto schedules work ahead of token expiry, proactively refreshing credentials so your API calls never fail due to a stale bearer token.
  • Per-Customer Overrides: If a specific customer's NetSuite instance uses non-standard custom fields for expense categorization, Truto supports per-account configuration overrides without forking the base integration.
Tip

Idempotency is mandatory. When reading from Brex and writing to an accounting system, network timeouts will happen. Always use idempotency keys when creating records in the accounting system via Truto to ensure a retried Brex webhook does not result in duplicate expenses.

Observability, Monitoring, and Troubleshooting

A Brex-to-accounting pipeline that works on day one and silently drifts on day 30 is worse than one that fails loudly. Observability is non-negotiable. There are three signal types to instrument.

Structured Logs

Every call to Brex and every write to the ledger should emit a structured log record with a consistent shape. At minimum:

{
  "event": "brex.expense.synced",
  "integrated_account_id": "acc_9f2",
  "brex_expense_id": "exp_01H...",
  "ledger_provider": "quickbooks",
  "ledger_record_id": "1247",
  "amount_minor": 8942,
  "currency": "USD",
  "latency_ms": 412,
  "attempt": 1,
  "result": "success",
  "trace_id": "01HZ..."
}

The trace_id should propagate from the incoming Brex webhook through every downstream call. When a customer reports a missing expense, you should be able to find the trace in under a minute.

Key Metrics

Export these as time-series to whichever observability stack you use:

Metric Why It Matters
brex_api_requests_total{status} Detect rate-limit spikes and 5xx bursts per Client ID.
brex_api_latency_seconds (p50, p95, p99) Latency regressions are the earliest signal of upstream issues.
ledger_write_success_total{provider} Track write success per ERP to spot provider-specific outages.
sync_lag_seconds{account_id} Time between Brex webhook receipt and ledger write completion. Alert if > 5 minutes.
dlq_depth{queue} Dead-letter queue depth. Any non-zero sustained value is a bug or an outage.
token_refresh_failures_total{provider} OAuth token refresh failures are silent killers; page on any threshold.

Troubleshooting Playbook

When a customer reports "my Brex expenses stopped syncing," work the checklist in this order:

  1. Is the OAuth connection alive? Check the last successful token refresh timestamp. A stale refresh token requires the user to re-authorize.
  2. Are webhooks being delivered? Check the Brex webhook subscription status and inspect the last 20 delivery attempts in your ingest log.
  3. Are events in the DLQ? Query the dead-letter queue filtered by integrated_account_id. Common causes: missing GL account mapping, unresolvable vendor, tax rate not found.
  4. Is the ledger side returning validation errors? Look for 400s from the accounting provider. They are almost always mapping problems, not infrastructure problems.
  5. Has the customer changed their Chart of Accounts? If they deleted or archived a GL account you had cached, every subsequent write will 400 until the cache invalidates.

Operational Runbook: Alerts, Retries, and Backfills

A runbook turns a 3 AM outage into a checklist instead of a panic. Here is the minimum viable runbook for a Brex accounting pipeline.

Alerts to Configure

Alert Condition Severity
Sync lag high sync_lag_seconds > 300 sustained for 10 minutes on any account P2
DLQ growing dlq_depth increases for 3 consecutive polling intervals P2
Token refresh failing Any token_refresh_failures_total increment P1
Brex 5xx spike brex_api_requests_total{status=~"5.."} > 5% of total for 5 minutes P2
Rate limit saturation brex_api_requests_total{status="429"} > 1% of total P3
Ledger write failures ledger_write_success_total drops > 20% week-over-week per provider P2
Webhook signature failures Any HMAC verification failure on inbound Brex webhooks P1 (security)

Retry and Dead-Letter Strategy

Retries live at two layers:

  1. Transport-level retries (in the HTTP wrapper): the exponential-backoff-with-jitter algorithm shown earlier. Bounded to ~6 attempts, ~2 minutes total.
  2. Business-level retries (in the queue): if transport retries are exhausted, the message goes back on the queue with a delay. After 3 business-level retries, it lands in the dead-letter queue with the full error context.

DLQ items should never be silently dropped. Every DLQ item is either (a) fixable by the customer (missing GL account mapping) - surface it in your admin UI, or (b) a bug on your side - page the on-call engineer.

Backfill Procedure

Backfills are needed after outages, after fixing a mapping bug, or when onboarding a new customer with historical data. The procedure that works:

  1. Identify the window. Determine the exact posted_at range that needs replay. Never backfill "everything" - it burns your Brex rate limit budget and often duplicates data.
  2. Run in a separate worker pool. Backfill jobs should never contend with real-time sync jobs. Use a dedicated queue and rate-limit it to a fraction of your normal request budget (e.g., 30% of the 1,000-per-60-seconds limit).
  3. Enforce idempotency at the ledger. Use a deterministic idempotency key like brex-<brex_expense_id> on every write. Re-running the backfill five times must produce the same ledger state as running it once.
  4. Log dry-run first. For any backfill touching more than 100 records, run once with writes disabled. Log what would be written. Have a human eyeball the sample before enabling writes.
  5. Reconcile at the end. After the backfill completes, count Brex expenses in the window and ledger records with the corresponding idempotency prefix. They should match exactly. Any diff goes to the DLQ for manual review.
Warning

Never backfill against a live customer's ledger without a rollback plan. If you write 8,000 duplicate expenses into a customer's QuickBooks because of a bad idempotency key, you own the manual cleanup. Test the backfill on a sandbox account first, every single time.

Where This Is Heading

The Brex-to-accounting integration pattern is a microcosm of a much larger trend. Brex recently announced a new AI-native Accounting API designed to automate accounting workflows end-to-end and enable deeper, real-time integrations with ERP systems. The Capital One acquisition will likely accelerate the expansion of Brex's API surface and the volume of transaction data flowing through it.

For engineering teams building spend management or fintech products, the decision is not whether to integrate Brex with accounting systems—your customers will demand it. The decision is whether you build and maintain the N×M matrix of point-to-point integrations yourself, or you invest that engineering time in your core product and let the plumbing run through an API layer designed for exactly this problem.

If you are evaluating your options, start with the hardest case. Try writing a multi-currency Brex expense to a NetSuite subsidiary with custom segments. If your integration approach handles that cleanly, everything else will be straightforward.

FAQ

How do I handle Brex API rate limits?
Brex enforces rate limits (e.g., 1,000 requests per 60 seconds) using HTTP 429 status codes. You must implement exponential backoff and cursor-based pagination in your application, utilizing standard IETF rate limit headers to determine when to retry the request.
Does the Brex API return real-time transaction data?
No. Brex only returns settled transactions through the Transactions API. Pending transactions are not available. Use the Webhooks API with EXPENSE_PAYMENT_UPDATED events for near-real-time notifications.
What is the best way to map Brex transactions to an accounting ledger?
You must dynamically fetch the destination system's Chart of Accounts, Vendors, Tax Rates, and Tracking Categories. This allows you to build a mapping interface so users can link their Brex spend to the correct double-entry accounting segments.
Does Truto store my customers' financial transaction data?
No. Truto uses a zero-data-retention, pass-through architecture. It processes API payloads in memory to execute mappings and immediately forwards them without storing your customers' sensitive data.
What happened to Brex after the Capital One acquisition?
Capital One completed its acquisition of Brex in April 2026. The Brex API continues to operate normally, but engineering teams should monitor the developer changelog for any future infrastructure changes.

More from our Blog