ml-connector
XeroBrex

Xero and Brex integration

Xero runs accounting for small and medium businesses. Brex runs corporate cards, expenses, and bill pay. Connecting the two means the card spend and bills that Brex codes flow into Xero without re-keying, and the vendor and account masters stay in agreement. ml-connector handles the different APIs on each side and moves the data as Brex finishes coding each transaction. Because Brex card and cash transactions are read-only, the general ledger stays in Xero where it belongs.

How Xero works

Xero exposes contacts, invoices and bills, purchase orders, payments, chart of accounts, tracking categories, and manual journals through its REST Accounting API at api.xero.com. It authenticates with OAuth 2.0 authorization code, where access tokens last 30 minutes and refresh tokens last 60 days, and every call carries a Bearer token plus a Xero-tenant-id header that targets one organization. Xero pushes webhooks signed with HMAC-SHA256 in the x-xero-signature header, but those payloads carry only a resource id and event type, so a follow-up GET is needed to read the full record. List endpoints use page-based pagination, and the If-Modified-Since header fetches only records changed since a given UTC time.

How Brex works

Brex exposes coded card expenses, vendors, transfers, accounting records, and accounting fields through its REST Developer API, with modern endpoints under api.brex.com and payments and expense endpoints still on the platform.brexapis.com v1 base. It authenticates with a bearer token, either a static User Token issued in the dashboard or an OAuth 2.0 access token for partner apps, and the User Token expires after 90 days of inactivity. Card and cash transactions are read-only, while expenses, vendors, transfers, and accounting records support writes. Brex pushes Svix-signed webhooks, and the ACCOUNTING_RECORD_READY_FOR_EXPORT event fires once a transaction is coded and ready for ERP export. List endpoints use cursor-based pagination.

What moves between them

The main flow runs from Brex into Xero. Once Brex marks an accounting record or coded card expense ready for export, ml-connector reads it and posts it into Xero as a bill of type ACCPAY or a manual journal, mapped to the matching Xero account and contact. Vendor records flow the same direction so Xero supplier contacts reflect the Brex vendor directory. Reference data such as the Brex chart of accounts mapping and accounting fields is aligned with Xero accounts and tracking categories so each line lands on a valid dimension. Brex card and cash transactions are read-only, so ml-connector never writes ledger entries back into Brex; it only marks each record exported.

How ml-connector handles it

ml-connector stores both credential sets encrypted. On the Brex side it sends the bearer token on every request and tracks the User Token, since it expires after 90 days without a call, so a heartbeat keeps it alive. On the Xero side it refreshes the 30-minute access token before expiry, renews the 60-day refresh token on use, and sends the Xero-tenant-id header so every post lands in the right organization. The trigger is the Brex ACCOUNTING_RECORD_READY_FOR_EXPORT webhook, verified with its Svix headers, after which ml-connector calls GET on the accounting record to read the coded transaction, then posts it into Xero. A scheduled poll over Brex records and expenses backfills anything a webhook missed, paging by next_cursor and capping the Expenses API at 100 per page after the February 2026 limit change. Accounts and tracking categories are mapped first, so every line references a Xero account and a contact that already exist. Brex merchant names rarely match Xero contact names exactly, so a fuzzy-match and caching layer resolves each merchant to the right supplier. Neither side offers a Stripe-style idempotency header, so ml-connector dedupes on the Brex record id and the Xero GUID before posting, supplying the GUID on create as an upsert, then calls Xero, and on success marks the Brex record EXPORTED. Brex rate limits return HTTP 429 with no documented headers, so ml-connector backs off with jitter, and every record carries a full audit trail and can be replayed if a Xero post fails.

A real-world example

A mid-sized software firm of about 120 people runs Xero for accounting and gives staff Brex corporate cards for software, travel, and vendor bills. Before the integration, an accountant exported card statements from Brex each week and keyed the coded spend into Xero by hand, which meant expenses sat uncategorized for days, merchant names were typed differently each time, and the card clearing account never tied out at month-end. With Xero and Brex connected, each coded card expense and bill posts into Xero as it becomes ready for export, allocated to the right account and tracking category, and vendors stay aligned with Xero contacts. Close starts with the card spend already booked, and the weekly re-keying step is gone.

What you can do

  • Post Brex coded card expenses and accounting records into Xero as bills or manual journals once they are ready for export.
  • Keep Xero supplier contacts aligned with the Brex vendor directory using fuzzy merchant matching.
  • Map Brex accounting fields to Xero accounts and tracking categories so each line lands on a valid dimension.
  • Authenticate Brex with its bearer token and Xero with OAuth 2.0 and the per-organization tenant header.
  • Trigger on the Brex ready-for-export webhook with scheduled backfill, dedup, retries, and a full audit trail on every record.

Questions

Which direction does data move between Xero and Brex?
The main flow is Brex into Xero. Coded card expenses, accounting records, and vendors move from Brex into Xero as bills, manual journals, and supplier contacts. Brex card and cash transactions are read-only, so the general ledger stays in Xero and ml-connector only marks each Brex record exported, never writing ledger entries back.
How does the integration know when a Brex transaction is ready to post?
Brex pushes the ACCOUNTING_RECORD_READY_FOR_EXPORT webhook once it finishes coding a transaction. ml-connector verifies the Svix signature, then calls GET on the accounting record to read the coded values and posts it into Xero. A scheduled poll over Brex records and expenses backfills anything a webhook missed, paging by cursor.
How are duplicate postings avoided without a shared idempotency key?
Neither Xero nor Brex offers a Stripe-style idempotency header for these records. ml-connector dedupes on the Brex record id and the Xero GUID, supplying the GUID on create so Xero treats the call as an upsert. After Xero accepts the post it marks the Brex record EXPORTED, so a re-read transaction is never booked twice.

Related integrations

Connect Xero and Brex

Free to use. Add your credentials, ping your real systems, and see if we fit.

Get started