---
name: brex-oauth-app
description: Obtains Brex OAuth 2.0 credentials — client ID, client secret, redirect URIs and scopes — for a platform that connects other organizations' Brex accounts. Covers the fact that Brex OAuth apps are partner-gated rather than self-serve (there is no developer portal that issues credentials), the route into the partner programme, the per-customer user-token alternative and its admin runbook, the full scope catalogue and its read/write split, staging access, 1-hour access tokens with rotating 90-day refresh tokens, rate limits, and a safe credential handoff. Use when asked to get Brex OAuth or API credentials, register a Brex app, become a Brex developer partner, connect customer Brex accounts, set up Brex staging access, or fix a Brex authorization error such as a rejected redirect URI, a missing scope, a 401 after an hour, or a refresh that stops working. For any other vendor's developer portal, use that vendor's skill instead.
---

# Brex OAuth2 App Registration

Get working Brex credentials for a platform that reads and writes data in *other companies'* Brex accounts — and
decide, before anything else, whether that is even the credential you can have.

**Brex has no self-serve OAuth app registration.** There is no developer portal page where you create an app,
paste redirect URIs and receive a client ID. Brex's own partner authentication guide says to "contact our
developer support team to be issued a client ID and client secret", and the redirect URIs are ones you supply to
Brex "when the credentials were set up". OAuth on Brex is a **partner programme with a signed agreement, a
staging build, a demo review and human-delivered credentials** — measured in weeks, not minutes.

That shapes the whole task. If the partnership is not in place, the only credential a customer can give you today
is a **user API token** that they generate themselves in their own Brex dashboard. In that case the deliverable is
not an app registration at all: it is a **per-customer admin runbook** (§9), and you should say so plainly rather
than looking for a registration page that does not exist.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **Is there already a Brex partnership / issued client ID?** | Decides the whole run — see §1 |
| **Company legal name, counterparty address** | Brex asks for these to draft the API access agreement (§2) |
| **Name, email and title of the person who will sign** | Brex's agreement is signed by a human at your company (§2) |
| **A staging account email** | Brex provisions a staging Brex account and invites this address (§2, §7) |
| **Redirect URIs** | Every callback host; supplied to Brex at credential setup, not self-edited later (§3) |
| **Which Brex data the connector reads and writes** | Drives the scope list Brex issues for your client (§4) |
| **Read-only or read/write?** | Write scopes reach cards, transfers and users — a different conversation (§4) |
| **A demo you can record** | Brex generates production credentials after you show a working staging demo (§2) |
| **If no partnership: which customer, and who is their Brex account admin?** | The user-token runbook needs a named admin (§9) |

## Quick Start

1. Decide the route: partner OAuth, or per-customer user tokens (§1). **Do this first.**
2. If partner OAuth: start the partner conversation and work the onboarding sequence (§2).
3. Hand Brex every redirect URI at credential-setup time (§3).
4. Agree the exact scope strings your client will be issued (§4).
5. Capture client ID, client secret and the auth-server URLs, per environment (§5).
6. Build for 1-hour access tokens and **rotating** 90-day refresh tokens (§6).
7. Test in staging, and understand what staging is *not* (§7).
8. Design around rate limits, idempotency and the single-webhook-endpoint rule (§8).
9. If there is no partnership: deliver the per-customer admin runbook instead (§9).
10. Verify a real authorize → callback → token → refresh → read round trip (§10).
11. Hand the credentials over — never commit them (§11).

## Platform state (verified 2026-09-20 — re-verify before trusting)

Five facts decide this task. Check the linked pages before following any older runbook:

- **OAuth credentials are issued by humans, not by a portal.** Brex's partner authentication guide: "Please
  contact our developer support team to be issued a client ID and client secret for partner authentication."
  Developer support is `developer-support@brex.com`. There is no app-creation screen anywhere in Brex's developer
  documentation, and the developer area of the Brex dashboard creates **user tokens**, not OAuth apps.
- **Partner onboarding is a sequence with an agreement and a demo in it.** Brex's partner onboarding page: once
  your application is approved you receive **staging** credentials over SendSafely, which "expire in 7 days"; you
  provide company name, counterparty address and the signer's name, email and title; you then receive a
  **Unilateral API Access Agreement** from Brex; and "once you've built your demo, share it with us in a video and
  we'll generate production credentials for you."
- **Redirect URIs are a field you give Brex, not one you edit.** "Partners must supply one or more redirect URIs
  that Brex will use to post the authorization code", and the `redirect_uri` on the authorize call "must match
  exactly one of the addresses that was provided to Brex when the credentials were set up." Plan the full list up
  front; adding one later is a support request.
- **Base URLs moved on 2026-01-01, and the old ones still work.** Brex: "As of January 1, 2026, we are
  transitioning base URLs to `api.brex.com` and `api-staging.brex.com` respectively, but the former URLs
  (`platform.brexapis.com` and `platform.staging.brexapps.com`) will continue to be available for the forseeable
  future." Auth servers are separate hosts and did **not** move: `accounts-api.brex.com/oauth2/default` and
  `accounts-api.staging.brexapps.com/oauth2/default`.
- **There is no public sandbox.** Staging is partner-only, it is handed out as part of onboarding, it carries no
  availability guarantee — "data stored in staging environments may be purged at any time" — and Brex's own API
  references label the staging server "Note: This is not a sandbox. It will not work with customer tokens."

Brex also publishes live OpenID Connect discovery documents for both environments, and tells you to treat them as
authoritative: "This may change in the future without notice. Always use the well-known OpenID configuration
document as the source of truth for the base URL." Read them before you trust this page (§References).

If the documentation does not look like this, stop and report what you actually see rather than clicking on.

## 1. The route decision — read this first

Brex's own FAQ draws the line in one sentence: "The user token is used to authenticate your own Brex account. The
OAuth token is used when building partner applications to authenticate other Brex accounts."

So for a multi-tenant platform, **OAuth is the correct model and the user token is the fallback.** But the
correct model is gated and the fallback is not, and that asymmetry is the whole decision:

| | Partner OAuth | Per-customer user API token |
| --- | --- | --- |
| Who issues it | Brex developer support, after partner onboarding (§2) | The customer's own Brex admin, self-serve in their dashboard |
| Lead time | Weeks: application, agreement, staging build, demo review | Minutes |
| Onboarding UX | Redirect-based consent from your product | Customer copies a secret and pastes it into your product |
| Credential held | One client ID/secret, plus a token pair per customer | One long-lived bearer token per customer |
| Token shape | `access_token` (1 hour) + rotating `refresh_token` (90 days) | `bxt_…` bearer token, no refresh |
| Expiry | Access hourly; refresh must be exchanged inside 90 days | Expires after **90 days of no API calls**; dies if the user is deactivated |
| Scope control | The scope list Brex issues for your client; customer consents per scope | Whatever the customer happens to tick when creating the token |
| Scales to self-serve signup | Yes | No — every customer is a manual handoff |

**Pick partner OAuth if the platform will connect Brex accounts as a product feature.** It is the only route that
gives redirect-based onboarding, revocable per-customer grants and a credential your customer never has to handle.
Start §2 today, because the lead time is the long pole.

**Pick user tokens if you need to connect a handful of named customers now**, or while the partnership is in
flight. It works, it is documented, and it is the customer's own account admin authorizing their own account —
which is exactly what Brex intends it for. What you owe the customer in that case is §9's runbook, and what you owe
your own team is the knowledge that this does not scale and that each token is a long-lived secret with no
rotation story built in.

**Do not mix the two silently.** A connection made with a user token and a connection made with OAuth have
different expiry behaviour, different revocation paths and different scope granularity. Whatever stores the
credential must know which kind it holds.

## 2. Getting in: the partner programme

There are two doors and they lead to the same room.

- **Developer support** — `developer-support@brex.com` — is the technical door, and the one Brex's partner
  authentication guide points at for client ID and secret. Brex asks that you include the request trace ID from
  the `X-Brex-Trace-Id` response header on any API-error report, and warns you not to send tokens or
  authorization headers in support mail.
- **The partner team** — via the "Become a partner" form on Brex's partners page — is the business door. Brex
  groups partners as embedded finance, **technology partners** ("Integrate your software natively with Brex"),
  accounting firms, PE/VC and service partners. A multi-tenant data connector is a technology partner.

Brex also runs a developer Slack community, linked from its contact page; useful for questions, not for
credentials.

**The documented onboarding sequence**, in order:

1. **Application and approval.** Brex approves the application before anything is issued.
2. **Staging credentials arrive over SendSafely, and expire in 7 days.** Treat the SendSafely link as perishable:
   whoever receives it must be ready to move the values straight into the secret store. If it lapses, you ask
   again.
3. **Agreement paperwork.** You supply company name, counterparty address, and the signer's name, email and
   title; Brex sends a **Unilateral API Access Agreement**.
4. **A staging Brex account, on request.** "If you need access to an account on staging, provide us with an email
   address and we'll provision a staging account for you and invite you to it." Ask for this explicitly — without
   it you have credentials but no data to authorize against.
5. **Build the demo.**
6. **Share the demo as a video.** "Once you've built your demo, share it with us in a video and we'll generate
   production credentials for you" — also delivered via SendSafely.

**What this means for planning.** Production access is gated on a working integration, so the sequencing is
staging-first: you cannot get production credentials, then build. Budget for the agreement's legal review on your
side, and for the fact that the demo is a real deliverable.

**What is *not* documented**, and therefore what you must not promise: Brex publishes no security questionnaire,
no penetration-test requirement, no connection cap, no listing or marketplace review, and no stated turnaround
time for any step. If the user needs those answers — because procurement, security review or a launch date
depends on them — that is a question for the Brex partner team, not something to infer (§Stop and ask).

## 3. Redirect URIs

Give Brex **every** callback host your platform serves, in the same message as the rest of the setup details.
There is no self-service editor: the addresses are "provided to Brex when the credentials were set up", and the
`redirect_uri` you send on the authorize call must match one of them **exactly**.

For Unified.to these are one per data center; confirm the current list with the platform owner rather than
assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Notes:

- **Register all four on day one**, including `api-dev`. Adding a data center later is an email and a wait, not a
  form submission — and a region that launches without its callback registered is a region that cannot connect.
- **Exact match means exact**: scheme, host, path, case and trailing slash. A URI that is "obviously the same" is
  not the same.
- Ask Brex to confirm, in writing, the exact list they stored, and keep that reply. It is the only copy of that
  configuration you control.
- **Staging and production credentials are separate**, so confirm the redirect list is attached to both sets.

## 4. Scopes

Scopes are **space-delimited** on the authorize URL, and Brex states that "the scopes for your client will be
provided to you" once you are a registered partner — that is, the catalogue below is what exists, and what your
client may request is what Brex enables for it. Agree the list explicitly during onboarding.

Two scopes are structural rather than data-bearing, and a connector needs both:

| Scope | Why |
| --- | --- |
| `openid` | Required to make an OpenID Connect request |
| `offline_access` | **Required to obtain a refresh token** — omit it and every connection dies in an hour |

The OIDC discovery document also advertises the standard identity scopes `profile`, `email`, `address` and
`phone`. They return claims about the signed-in user; they return no business data.

### The catalogue, exactly as Brex spells it

| API | Read | Write |
| --- | --- | --- |
| **Transactions** | `accounts.card.readonly`, `accounts.cash.readonly`, `transactions.card.readonly`, `transactions.cash.readonly`, `statements.card.readonly`, `statements.cash.readonly` | *(none — this API is read-only)* |
| **Expenses** | `expenses.card.readonly` | `expenses.card` |
| **Budgets** | `budgets.readonly` | `budgets` |
| **Team** | `cards.readonly`, `companies.readonly`, `departments.readonly`, `legal_entities.readonly`, `locations.readonly`, `titles.readonly`, `users.readonly` | `cards`, `departments`, `legal_entities`, `locations`, `titles`, `users`; plus `cards.pan` for card numbers |
| **Payments** | `transfers.readonly`, `vendors.readonly`, `linked_accounts.readonly` | `transfers`, `vendors`, `incoming_transfers` |
| **Accounting** | `accounting.integration.read`, `accounting.record.read` | `accounting.integration.write`, `accounting.record.write` |
| **Fields** | `fields.read`, `field_values.read` | `fields.write`, `field_values.write` |
| **Travel** | `travel.trips.readonly` | `travel.trips` |
| **Onboarding** | — | `https://onboarding.brexapis.com/referrals` |

Traps worth more than the table:

1. **The naming convention is not uniform.** Most pairs are `x.readonly` / `x` (cards, users, budgets, transfers,
   vendors, travel). But **Accounting and Fields use `.read` / `.write`** instead. Guessing `accounting.record.readonly`
   or `fields.readonly` produces a scope that does not exist.
2. **Expenses is spelled `expenses.card`, not `expenses`** — even though the current list endpoint is the
   un-suffixed `/v1/expenses` path, its published OpenAPI requires `expenses.card.readonly` or `expenses.card`.
   This is the single easiest scope in the catalogue to get wrong, because the endpoint name and the scope name
   disagree.
3. **The Onboarding scope is a URL.** `https://onboarding.brexapis.com/referrals` is the literal scope string.
   That API is also the one exception to the grant type: onboarding uses the **client credentials grant**, with no
   Brex user involved; every other API uses the authorization code grant.
4. **`cards.pan` is the one that reads actual card numbers.** Brex separates it from `cards` deliberately. Request
   it only if the platform genuinely needs the PAN, and expect it to change the compliance conversation with both
   Brex and your customers.
5. **Scopes are additive, per user.** Brex: adding functionality later means requesting the new scopes, "which
   will send the user through the authentication flow again and add those scopes to their previously consented
   scopes." Adding a scope does **not** upgrade tokens already issued.
6. **Consent is partial by design — this is the multi-tenant gotcha.** Brex: the client "may accept 0 to all of
   the requested scopes which your application will then have access to", and the callback returns
   `scope=<SCOPES>` listing what was actually granted, which "can be a subset of what was requested." A connector
   that assumes it got what it asked for will 403 on individual endpoints for individual customers, with nothing
   wrong at the app level. **Read the granted scope list off the callback, store it with the connection, and let
   it drive which objects you expose for that customer.**

### Who can consent

"Most Brex APIs require a Brex admin to grant access. In these cases, only users with the account admin role can
be authenticated." **Bookkeepers** may access the **accounting API** only. So the person who clicks Connect in
your product must be an account admin, or the flow fails for reasons your UI cannot explain — say so in the
connect screen.

If your users are accountants or bookkeepers who hold several Brex logins, Brex supports forcing a fresh login by
appending **`prompt=login`** to the authorize request. Without it, a servicer silently reconnects the same Brex
account they last signed into.

### Extra approval

Brex's published documentation does **not** describe a per-scope approval step: the approval gate is the
partnership itself, and the scope list is settled with Brex when your client is created. Treat "which of these
will Brex actually enable for us, and under what conditions" as a question for the partner conversation — not as
something to assume from the catalogue (§Stop and ask).

## 5. Capture the credentials

What you receive from Brex: a **client ID** and **client secret**, per environment, delivered over SendSafely
(staging first, production after the demo). Nothing self-serve regenerates them, so store them carefully the first
time — and remember the staging link expires in 7 days.

Endpoints, from Brex's partner guide and confirmed against the live OIDC discovery documents on 2026-09-20:

| | Production | Staging |
| --- | --- | --- |
| Auth server base | `https://accounts-api.brex.com/oauth2/default` | `https://accounts-api.staging.brexapps.com/oauth2/default` |
| Authorize | `…/v1/authorize` | `…/v1/authorize` |
| Token | `…/v1/token` | `…/v1/token` |
| Revoke | `…/v1/revoke` | `…/v1/revoke` |
| Userinfo | `…/v1/userinfo` | `…/v1/userinfo` |
| Discovery | `…/.well-known/openid-configuration` | `…/.well-known/openid-configuration` |
| API base | `https://api.brex.com` (formerly `https://platform.brexapis.com`) | `https://api-staging.brex.com` (formerly `https://platform.staging.brexapps.com`) |

The authorize request takes `client_id`, `response_type=code`, `redirect_uri`, `scope` (space-delimited) and
`state`. **`state` is required and "must be longer than 8 characters"** — Brex enforces a length, not just a
presence. Verify it on the way back.

**PKCE is supported but not required.** Discovery advertises `code_challenge_methods_supported: ["S256"]`. If the
client can send a code challenge, send one; it costs nothing and closes the code-interception hole.

The token exchange is `application/x-www-form-urlencoded` with `grant_type=authorization_code`, `code`,
`redirect_uri`, `client_id` and `client_secret` **in the body** — `client_secret_post`. Discovery also advertises
`client_secret_basic`, so HTTP Basic works if your HTTP client prefers it; pick one and be consistent.

The response carries `access_token`, `refresh_token` and `token_type: "bearer"`. Calls then send
`Authorization: Bearer <ACCESS_TOKEN>`.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the
transcript and that replacing it means going back to Brex, then move on.

## 6. Token lifetimes and rotation

Four facts, and a connector that misses any one of them breaks later rather than now.

| Fact | Consequence |
| --- | --- |
| **Access token: 3600 seconds (1 hour)** | Refresh proactively; a stale token is a 401 on every call |
| **Refresh returns a new refresh token as well as a new access token** | Persist the new refresh token from *every* exchange, or the connection dies at the next refresh |
| **Refresh tokens "expire every 90 days"** | A connection nobody syncs for three months requires the customer to re-authorize |
| **Revocation is `POST {auth base}/v1/revoke`** with `token`, `client_id`, `client_secret` | This is how you cleanly disconnect a customer — use it on disconnect rather than dropping the row |

More on each:

- **Rotation is the silent killer.** Brex is explicit that the refresh response "will contain a new
  `<ACCESS_TOKEN>` as well as a new `REFRESH_TOKEN`". A client that stores only the value it got at connect works
  until the first refresh and then fails permanently, with no user-visible event. Treat "we store the token we got
  at connect" as a defect, not a design.
- **The 90-day clock is about exchange, not idle time per se.** Brex: refresh tokens "should be exchanged for a
  new one before then. Failure to do so will require the user to re-authenticate." An actively-synced connection
  stays alive indefinitely; a dormant one does not. If the platform has a pause/disable feature, make sure it does
  not also pause refreshes.
- **Client credentials tokens do not refresh.** The onboarding API's client-credentials flow returns a 1-hour
  token and no refresh token — Brex's advice there is simply to request a new one when it expires.
- **User tokens are a different clock entirely** (§9): they expire after **90 days without an API call**, and they
  stop working the moment the underlying Brex user is deactivated.

## 7. Staging is not a sandbox

This matters more here than for most vendors, because the alternative to a test environment is experimenting
against real company money.

- **Staging is partner-only.** It arrives with your onboarding credentials; there is no public signup.
- **Brex labels it explicitly**: "Note: This is not a sandbox. It will not work with customer tokens." Staging is
  a separate Brex world with separate credentials and a separately provisioned Brex account — not a mode of the
  production one.
- **No availability guarantee, and data can vanish**: "We can remove data from the staging environment at any
  time, so please do not use this in anything that's critical or a production use-case… Use staging for
  point-in-time testing only." Do not build regression fixtures that assume a staging record still exists.
- **Ask for a staging Brex account explicitly** (§2, step 4). Credentials without an account to authorize against
  prove only that the token endpoint answers.
- **Production is the only place your customers live**, and every read there is real financial data. Scope
  minimally (§4), and treat any write path — transfers especially — as needing a deliberate, separate sign-off.

## 8. Limits, idempotency and webhooks

None of this is fixed by re-registering; design for it.

- **Rate limits, per client ID *and* Brex account**: up to **1,000 requests in 60 seconds**, **1,000 transfers in
  24 hours**, **100 international wires in 24 hours**, **5,000 cards created in 24 hours**. Exceeding one returns
  **HTTP 429**. Brex's guidance: prefer webhooks over polling, back off exponentially "with some randomness", and
  throttle client-side with a token bucket per client ID. Contact support for higher limits. Note the scoping —
  one noisy customer cannot exhaust another customer's budget, but your client ID is one half of the key.
- **Page sizes are not uniform, and they change.** The Expenses list endpoints had their **maximum `limit`
  reduced from 1000 to 100 in February 2026**. A connector with one global page-size constant will start getting
  400s from exactly one API. Cap per endpoint, from each API's own reference.
- **Idempotency keys are required on the dangerous endpoints.** `Idempotency-Key` is optional on most POST/PUT
  calls and **required** on Create transfer and Create card. Brex's rules: keys must be "random strings with
  sufficient entropy to avoid collisions like V4 UUIDs"; your application "must not provide identical idempotency
  keys for different requests"; and it "should store these keys so that semantically identical requests are sent
  with the same idempotency keys." **A key regenerated on each retry defeats the entire mechanism** — the retry of
  a timed-out transfer becomes a second transfer. GET and DELETE ignore the header.
- **Webhooks exist but cover very little.** The published catalogue is six events: `REFERRAL_CREATED`,
  `REFERRAL_ACTIVATED`, `REFERRAL_APPLICATION_STATUS_CHANGED` (Onboarding), `TRANSFER_PROCESSED`,
  `TRANSFER_FAILED` (Payments), `USER_UPDATED` (Team); an `ACCOUNTING_RECORD_READY_FOR_EXPORT` event was added in
  March 2026. There is **no** webhook for expenses or for card/cash transactions — those must be polled.
- **One webhook endpoint per customer per client ID.** Brex: "Currently only one webhook endpoint can be
  registered per customer / `client_id`" — though one endpoint may subscribe to many event types. Registration is
  by API (`POST /v1/webhooks`), and you must verify the signature on receipt.
- **Transactions lag, so poll with an overlap.** Brex documented in July and August 2026 that card and cash
  transaction lists "may reflect a small lag" and recommended "a lookback window of at least one day when syncing
  transactions so delayed transactions are not missed", and an overlapping `posted_at_start` when polling. A
  poller whose high-water mark advances to "now" with no overlap **will silently drop late-posting transactions** —
  and because the API returns only settled transactions, late posting is normal, not exceptional.
- **Pending transactions are not available at all.** Brex's FAQ: "Only settled transactions are returned from the
  API." Do not promise real-time spend visibility.

## 9. No partnership yet? Ship the admin runbook

When the answer to §1 is user tokens, the deliverable per customer is this. Send it to the customer's **Brex
account admin or card admin** — nobody else can do it.

> **Connecting your Brex account**
>
> 1. Sign in to `https://dashboard.brex.com/` as an **account admin** or **card admin**.
> 2. Go to **Developer → Settings** (`https://dashboard.brex.com/settings/developer`).
>    *If your company has not yet accepted the developer API agreement, you will be asked to review and accept the
>    terms first. This is a one-time, company-level step and it requires someone empowered to accept it.*
> 3. Click **Create Token**.
> 4. Name the token something you will recognise later — include our platform's name, so it is obvious what it is
>    for when someone audits your tokens.
> 5. **Select exactly these scopes** — no more: *(list the exact strings from §4 that the connector needs, and
>    nothing else)*.
> 6. Confirm the summary screen, then select **Allow Access**.
> 7. **Copy the token immediately.** It begins `bxt_` and **you will not be able to see it again**; the dashboard
>    shows only a partially obfuscated version afterwards. If you lose it, create a new one and replace it.
> 8. Send it to us through *(your secure channel — never email, chat or a ticket)*.

Tell them the three things that will otherwise surprise them later:

- **The token expires after 90 days without an API call.** Regular syncing keeps it alive; a paused connection
  quietly dies.
- **The token is tied to a Brex user.** If that person is deactivated, the token stops working — so create it
  under a durable admin, not someone's personal account that offboarding will close.
- **They can revoke it any time** from the same developer page, and calls "will immediately begin to fail".

And tell your own side: this is a long-lived bearer secret with no refresh and no rotation protocol. It needs the
same handling as any other customer credential, plus a plan for what happens when it expires (§11).

## 10. Verify end-to-end

1. **Read the discovery document** for the environment you are targeting and confirm the endpoints match what the
   client is configured with. Brex says to treat it as the source of truth.
2. Run a full **authorize → consent → callback** against a staging Brex account. Confirm the consent screen lists
   exactly the scopes you expect, and that `state` comes back matching what you sent.
3. **Read `scope` off the callback** and compare it to what you requested. Deliberately deselect one scope at
   consent time and confirm the connector degrades gracefully instead of 403-ing at random later (§4).
4. Exchange the code and confirm you received both an `access_token` and a `refresh_token`. If there is no refresh
   token, `offline_access` was missing.
5. Make one real read call with `Authorization: Bearer …`.
6. **Force a refresh, then force a second refresh using the token the first one returned.** Nothing else catches a
   client that ignores rotation before production does.
7. Call `/v1/revoke` with the refresh token and confirm the connection is cleanly torn down.
8. Repeat 2–6 in production, authorized by a real account admin who is not you.

| Symptom | Cause |
| --- | --- |
| No app-registration page anywhere in the Brex dashboard | Correct — OAuth apps are issued by Brex, not self-registered (§1, §2) |
| Authorize request rejected on `redirect_uri` | Not byte-identical to an address supplied to Brex at credential setup, or registered against the other environment (§3) |
| Authorize rejected with a `state` complaint | `state` is required and must be **longer than 8 characters** (§5) |
| Token exchange succeeds, no `refresh_token` in the response | `offline_access` was not requested or not granted (§4, §6) |
| Consent completes, then some endpoints 403 for some customers | Partial consent — the customer granted a subset; read the granted `scope` off the callback (§4) |
| Every endpoint 403 for one customer | Consent given by a non-admin, or a bookkeeper outside the accounting API (§4) |
| All calls 401 after an hour | Access token expired; refresh not wired up (§6) |
| Works once, then every refresh fails | Rotated refresh token not persisted (§6) |
| Connection dies after a quiet few months | Refresh token passed its 90-day window; customer must re-authorize (§6) |
| A `bxt_` token stops working with no change | 90 days without a call, or the Brex user behind it was deactivated (§9) |
| `403`/`401` in staging with a customer-supplied token | Staging "will not work with customer tokens" — it is not a sandbox (§7) |
| Staging data disappeared | Expected — staging data may be purged at any time (§7) |
| HTTP 429 | Rate limit: 1,000 req/60s per client ID and account; back off with jitter, poll less (§8) |
| Expenses list 400s on page size | Expenses `limit` max was reduced from 1000 to 100 in Feb 2026 (§8) |
| A retried transfer went out twice | Idempotency key regenerated on retry instead of reused (§8) |
| Transactions missing from a sync | No lookback overlap; late-posting settled transactions fell behind the high-water mark (§8) |
| A servicer keeps reconnecting the same Brex account | Add `prompt=login` to the authorize request (§4) |

When reporting any API error to Brex, include the **`X-Brex-Trace-Id`** response header — and strip tokens and
authorization headers from whatever you paste.

## 11. Hand off — never commit the secret

- **Do not** write the client secret, an access token, a refresh token, or a customer's `bxt_` user token into
  source control, a test, a fixture, a committed `.env`, a ticket, a PR body, or a chat channel. Brex says the
  same about user tokens: "Never check it into version control or save it somewhere publicly accessible."
- **Brex secrets are self-identifying.** A value beginning `bxt_` is a live Brex user token with whatever scopes
  its creator ticked, and on a production account that reaches real card, transaction and transfer data. If you
  ever see one in a diff, a log or a paste, treat it as compromised, say so, and tell the customer to revoke it
  from their developer dashboard immediately — revocation is instant and self-serve, so there is no reason to wait.
- **SendSafely links are perishable and single-purpose.** Move the values into the secret store on receipt; do not
  forward the link around.
- **Replacing a client secret means going back to Brex.** There is no self-serve rotation, so plan a rotation the
  way you would a migration, not a click.
- If a code change is needed (a redirect host, a scope, refresh-token persistence, per-endpoint page-size caps),
  keep it secret-free and say what the human must set out of band.
- Close with: which route was taken (partner OAuth or user tokens) and why; the partner onboarding stage reached
  and what is outstanding; client ID per environment and where each secret was delivered; the authorize, token and
  revoke URLs; the API base URL per environment; the exact scope strings agreed; the redirect URIs supplied to
  Brex and Brex's written confirmation of them; and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: nobody has decided between partner OAuth and per-customer user
tokens (§1 — it changes the product, not just the config); the Unilateral API Access Agreement needs signing, or
its terms need review; Brex asks for company details, a signer, or anything contractual (§2); someone needs a
committed answer on partner review timelines, security requirements, connection caps or listing — Brex publishes
none of these (§2); the scope list would include `cards.pan`, `transfers`, `incoming_transfers` or any write scope
that moves money or changes users (§4); the platform cannot store rotated refresh tokens (§6 — report it, do not
register and hope); a customer's production Brex account is about to be used as a test environment because staging
was not provisioned (§7); replacing a live client secret is on the table (§11); or the documentation does not
match the **Platform state** section above.

Never attempt to create Brex credentials through a logged-in dashboard session on the user's behalf, and never ask
a customer to send a `bxt_` token over an insecure channel.

## References

Verified 2026-09-20. `developer.brex.com` returns real HTTP 404s for paths that do not exist (checked), so the
status of these links is meaningful; each was also read on that date. Appending `.md` to a `/guides/…` path
returns the same page as Markdown, which is the fastest way to re-verify a quote.

- Brex API documentation home — https://developer.brex.com/
- Quickstart — https://developer.brex.com/guides/quickstart
- Authentication (user API tokens) — https://developer.brex.com/guides/authentication
- **Partner authentication (OAuth: endpoints, grants, PKCE, state, token lifetimes, revoke, redirect URIs)** — https://developer.brex.com/guides/partner_authentication
- **Partner onboarding (SendSafely, 7-day staging credentials, API access agreement, demo → production)** — https://developer.brex.com/docs/partner_onboarding/
- User roles, permissions and the full scope catalogue — https://developer.brex.com/guides/roles_permissions_scopes
- Rate limits — https://developer.brex.com/guides/rate_limits
- Idempotency — https://developer.brex.com/guides/idempotency
- Webhooks (event catalogue, one endpoint per customer/client ID) — https://developer.brex.com/guides/webhooks
- Changelog — https://developer.brex.com/changelog
- Contact us (developer-support@brex.com, `X-Brex-Trace-Id`, Slack community) — https://developer.brex.com/docs/support
- Brex API FAQ (user token vs OAuth token, settled-only transactions) — https://developer.brex.com/docs/faq
- OIDC discovery, production — https://accounts-api.brex.com/oauth2/default/.well-known/openid-configuration
- OIDC discovery, staging — https://accounts-api.staging.brexapps.com/oauth2/default/.well-known/openid-configuration
- Expenses API reference — https://developer.brex.com/openapi/expenses_api/
- Expenses API OpenAPI bundle (per-endpoint scopes) — https://developer.brex.com/_bundle/openapi/expenses_api.yaml
- Team API reference — https://developer.brex.com/openapi/team_api/
- Transactions API reference — https://developer.brex.com/openapi/transactions_api/
- Payments API reference — https://developer.brex.com/openapi/payments_api/
- Budgets API reference — https://developer.brex.com/openapi/budgets_api/
- Accounting API reference — https://developer.brex.com/openapi/accounting_api/
- Fields API reference — https://developer.brex.com/openapi/fields_api/
- Travel API reference — https://developer.brex.com/openapi/travel_api/
- Onboarding API reference — https://developer.brex.com/openapi/onboarding_api/
- Webhooks API reference — https://developer.brex.com/openapi/webhooks_api/
- Brex MCP server (OAuth and API-key auth; beta since April 2026) — https://developer.brex.com/docs/mcp
- Partner with Brex (technology partners, "Become a partner" form) — https://www.brex.com/partners
- Brex API help-centre article — https://www.brex.com/support/brex-api
- Brex dashboard — developer settings (where customers create user tokens) — https://dashboard.brex.com/settings/developer
- Brex status — https://status.brex.com/

## Product facts: the connector, as of 2026-09-20

A dated snapshot of how this platform's Brex connector behaves. Treat it as a starting point for a conversation
with the connector's owner, not as a specification.

**Authorization.** It authorizes against `https://accounts-api.brex.com/oauth2/default/v1/authorize` and exchanges
at `…/v1/token`, sending `client_id`, `response_type`, `redirect_uri`, `scope` and `state` on the authorize URL.
The exchange is a form POST carrying `client_id`, `client_secret`, `redirect_uri`, `code` and `grant_type` in the
body (`client_secret_post`); refresh sends `client_id`, `client_secret`, `redirect_uri`, `refresh_token` and
`grant_type`. Access tokens are presented as `Bearer`. **PKCE is not sent**, and no `prompt` parameter is sent —
both are optional on Brex's side (§5, §4), but `prompt=login` would matter for accountant/bookkeeper users.

**Environments.** It carries two host profiles: production on the legacy `https://platform.brexapis.com` API
hostname, and a Staging profile on `https://platform.staging.brexapps.com` with the staging auth server. Both
legacy hostnames remain served (§Platform state), but the connector has not moved to `api.brex.com` /
`api-staging.brex.com`.

**Second auth mode.** Besides OAuth, it accepts a customer-supplied API token presented as a bearer token, and its
customer-facing instructions point at the Brex dashboard's developer settings page — i.e. it already implements
§9's user-token route.

**Scopes.** Every capability's scope set begins with `offline_access openid profile email`, and a login-only set of
`openid profile email` is used for the identity flow. Per capability it then adds: `users.readonly`/`users`,
`locations.readonly`/`locations`, `departments.readonly`/`departments`, `transfers.readonly`, `cards.readonly`,
`transactions.card.readonly`, `companies.readonly`, `vendors.readonly`, `accounts.cash.readonly` and
`transactions.cash.readonly`. All of those match Brex's published per-endpoint requirements.

**Three things worth checking with the owner:**

1. **The expense capability requests no expense scope.** Its scope set is the `offline_access openid profile email`
   baseline and nothing else, while Brex's published OpenAPI for the expenses list and get endpoints requires
   `expenses.card.readonly` or `expenses.card`. Any customer authorized through this path would be granted no
   expense access, and expense calls should 403 — the read path appears to work today only for connections made
   with a user API token whose creator happened to tick the expense scope.
2. **The page-size ceiling is 1000 for all objects.** Brex reduced the Expenses list maximum from 1000 to 100 in
   February 2026 (§8), so a large page request against expenses is expected to fail.
3. **Two write-path details on Brex's dangerous endpoints.** The `Idempotency-Key` it sends on create and update is
   generated from the current millisecond-of-second plus a random number, freshly computed per attempt. That is
   both low-entropy against Brex's "V4 UUID"-grade guidance and, more importantly, **not stable across retries** —
   Brex's stated purpose for the header (§8). Separately, the cash-transaction poller advances its per-account
   high-water mark to the current timestamp with **no lookback overlap**, against Brex's July/August 2026 guidance
   to use at least a one-day overlapping window because settled transactions can post late.

**Credentials in source:** none found. No client ID, client secret or API token value appears in the connector's
Brex sources; the only credential-shaped strings are parameter names.
