Authentication
Audience: 🔌 Integration Client
This page covers the API key, the data-plane credential your integration uses. The admin session (login, 2FA, and how a key is first issued) is a one-time operator/admin task; it lives in the Operate track under Signing in & 2FA.
BankConnector has two credentials you actually use, split along a clean line:
| Credential | Header | Plane | What it's for |
|---|---|---|---|
| API key | X-API-Key: key_… | data plane | submitting payments, reading journal / reconciliation / statements, webhooks |
| Admin session | X-Session-Token: … | control plane | creating companies, creating/managing users, bank connections, approval policies |
Building a third-party add-on a customer installs into their own account, rather than an integration you run with a key you hold yourself? See "Application installs (OAuth 2.0, for add-on developers)" further down this page instead — a different, narrower credential the customer authorizes and can revoke on their own.
The distinction is deliberate and enforced: an API key cannot create companies or users, or set up connections. Those are workspace-admin actions and return 403. Payments are a machine job; platform administration is a User one.
If you provisioned a sandbox (POST /api/sandbox/provision), the response hands you both: the apiKey, and the dashboardLogin creds (subdomain/email/password) you log in with to get an admin session. (Sandbox tenants, and their credentials, are auto-deleted after 30 days of inactivity; provision a fresh one any time.)
The API key (data plane)
Your integration's day-to-day credential. A single header:
X-API-Key: key_<64 hex characters>
Key facts:
- The key is platform-bound and carries 256 bits of entropy (
key_+ 64 hex). - The key implies your platform. There is no tenant header and you do not pass
platformId; the server infers it from the key. You _do_ passcompanyId(which of your companies the request is for), in the body or query. A request whose stated scope doesn't match the key's platform is rejected (403 forbidden). - No expiry, no TTL, no refresh. A key is valid until revoked. Rotation is manual: create a new key, cut over, revoke the old one.
- Stored only as a SHA-256 digest server-side. It's shown to you once, at creation. Lose it and you rotate; it cannot be recovered.
⚠️ Scope reality: an API key is pinned to a platform, and
companyIdis free within that platform. If your company shares a platform with others, your key can address them by passing a differentcompanyId. For hard isolation, be the only company on your own platform. Confirm your tenancy model with your operator.
What an API key cannot do
API keys drive machine tasks: payments, webhooks, exports. They are deliberately blocked (403) from control-plane / customer-decision routes that must be an authenticated User's action:
- Creating companies (
POST /api/platforms/<id>/companies) - Bank connection setup and activation
- Approval-policy management
They also can't call session-only identity routes (/auth/me, /me/sessions, /2fa/*): those return 401 for a key.
User provisioning (capped): an API key can headlessly provision operators. It can create
approver/viewerusers (POST /api/users) and re-role among non-admin roles, so an ERP can onboard its own staff without a User in the dashboard. But a key is deliberately capped so it can't escalate to the control plane: it cannot create or grantadmin, set a password directly (its users are invite-only), reset a password, or erase a user. Those need an admin session. Still treat production keys as privileged: store them in a secret manager and rotate on staff changes.
Why: these are platform-administration and payment-safety decisions. Keeping them off the API key means a leaked integration key can move payments within its scope but can't reshape your tenant (mint users, spin up companies, or wire a new bank connection). To do those, an admin logs in with a session, covered in the Operate track, Signing in & 2FA.
Where does the key come from? An API key is minted once by a workspace admin, and only its metadata is listable afterwards. If you provisioned a sandbox the key is already in the
POST /api/sandbox/provisionresponse. The admin issuing flow (and the session auth behind it) is in Signing in & 2FA.
Required headers for a machine client
For a normal data-plane JSON request you need only:
X-API-Key: key_…
Content-Type: application/json # for JSON bodies (some routes take raw XML)
Idempotency-Key: <key> # on POST /api/journal/payments, see Idempotency
You do not send cookies, CSRF tokens, or an Origin. Server-to-server callers send no browser Origin, so the origin guard passes automatically. CSRF applies only to cookie-based browser sessions.
Optional headers worth knowing
| Header | Purpose |
|---|---|
BankConnector-Version: YYYY-MM-DD | pin the API version (baseline 2026-06-01). Echoed back on every response, including errors. A malformed date is a 400. |
X-Request-ID | set by the server on every response. Log it; support can look up any request by it. |
Idempotency-Replayed: true | response header telling you a payment was a replay, not a fresh execution. |
Auth failures
| Situation | Status | code |
|---|---|---|
| Missing / invalid credential | 401 | unauthorized |
Credential valid but wrong scope (e.g. a companyId outside the key's platform) | 403 | forbidden |
API key on most control-plane routes (e.g. POST /api/platforms/<id>/companies, approval policies) | 403 | forbidden |
API key on the bank-connection routes (/connections, /connections/<id>/…) | 403 | admin_required |
API key on a session-only identity route (/auth/me, /me/sessions, /2fa/*) | 401 | unauthorized |
⚠️ Don't branch on admin_required alone. Most control-plane denials come back as plain 403 forbidden; only the /connections/* family emits admin_required. Branch on the 403 status and treat both codes the same way: you need an admin session, and retrying with the API key will never succeed.
A 401 means re-authenticate (check the key or log in again). A 403 means you'll never get in with this request as written: an API key on a control-plane route is a 403 no matter how many times you retry; switch to an admin session.
Application installs (OAuth 2.0, for add-on developers)
Audience: add-on / ERP-plugin developers, distinct from everything above. If you operate your own integration with your own API key, skip this section — it's for a third-party add-on a customer installs into _their_ BankConnector account, so a stock OAuth 2.0 client library works against us with no bespoke SDK.
This is a standards-based flow (RFC 6749 §4.1 + RFC 7636 PKCE, RFC 8252 for native apps), so most of it you already know from any other OAuth provider. The one thing worth reading closely is the scope section below — it decides exactly what your add-on can touch, and it is narrower than an API key.
The authoritative source: RFC 8414 discovery
Point your OAuth client library at:
GET /.well-known/oauth-authorization-server
This RFC 8414 metadata document is generated from what the server actually enforces — scopes_supported and grant_types_supported are read from the same arrays the authorization and token endpoints validate against — so it cannot advertise a capability the server doesn't have. Fetch it and configure your library from it rather than hardcoding the endpoint URLs below; they're included here for readers, not as the source of truth.
The flow
GET /oauth/authorize— your add-on opens this in the system browser (RFC 8252: never an embedded webview) with the standard RFC 6749 §4.1.1 query parameters:response_type=code,client_id,redirect_uri,scope(space-delimited, drawn from the vocabulary below),state,code_challenge,code_challenge_method=S256. PKCE is mandatory — there is no client secret in this flow at all; a public native-app client authenticates itself by proving it holds thecode_verifiermatching thecode_challengeit registered.- The signed-in Admin who owns the account sees a consent screen and approves or declines. On approval the browser is redirected to your registered loopback address (
http://127.0.0.1:<any port>/<your path>— exact match;localhostis never accepted) carrying a single-use authorizationcode(10-minute TTL) and yourstate. POST /oauth/token(application/x-www-form-urlencoded) exchanges the code +code_verifierfor a token pair:access_token(bc_oat_…) — 1 hour lifetime, sent asAuthorization: Bearer <access_token>against the product API exactly like an API key.refresh_token(bc_ort_…) — 180 days, and rotates on every use:grant_type=refresh_tokenreturns a new refresh token, and the old one is immediately dead. Replaying an already-rotated refresh token is treated as theft and revokes the entire token family — so a client that loses track of the current refresh token (a crash mid-rotation, a race between two processes sharing one token store) needs the user to re-consent, by design.POST /oauth/revoke(RFC 7009) revokes a token your add-on no longer needs — on uninstall, for example. It always answers200, including for an already-revoked, expired, or unknown token (RFC 7009 §2.2's anti-oracle requirement: the endpoint must not leak which tokens exist).
⚠️
/oauth/tokenand/oauth/revokedo NOT use this API's usual error envelope. Every other endpoint in this product answers an error as{ "error": { "code", "message" } }(see Errors & Retries). These two are the standards-mandated exception: they answer the flat RFC 6749 §5.2 shape,{ "error": "invalid_grant", "error_description": "…" }, because that is what an off-the-shelf OAuth client library parses. Don't reuse yourerror.codehandling here.
There is deliberately no device-flow (RFC 8628) entry in the machine-readable OpenAPI security scheme — OpenAPI 3.1's flows object has no shape for it. It's not gone; it just isn't expressible in that one document. Ask your BankConnector contact if your integration needs it.
Scopes: what your add-on can actually reach
An install's authority is its grant — the set of scopes the authorizing Admin approved — never the identity of the human who clicked "Allow". The API Reference states this per operation: every endpoint an install can call carries an OAuth2 security option listing the exact scope it needs, right beside the ApiKeyAuth / SessionToken options a first-party integration would use instead. If an operation's reference entry carries no OAuth2 option, no grant reaches it — see the two carve-outs below before assuming that's an oversight.
The full grantable (delegable) vocabulary:
| Scope | Covers |
|---|---|
payments:submit | Submitting payments (POST /journal/payments and previews). |
payments:read | Reading journal documents / reconciliation data. |
statements:read | Reading account statements and balances. |
engine:use | The stateless conversion/validation engine (/convert, /validate, bank + payment-type discovery). |
banks:read | Reading bank connectivity/profile metadata. |
webhooks:manage | Registering and managing webhook endpoints. |
Request only what your add-on needs — the consent screen shows the Admin exactly this list, by name, and a narrower request is both a better trust signal and less to lose if your stored token ever leaks.
Some actions can never be delegated to any add-on, whatever the authorizing Admin holds: bank connection setup/activation, approval-policy management, approval decisions themselves, and managing other add-ons' grants. These require a live, signed-in human Admin session every time — that's a deliberate security boundary (a compromised or malicious add-on must never be able to redirect where payments go, or weaken the approval control that exists to contain it), not a gap in the scope list.
⚠️
webhooks:manageis in the grantable vocabulary, but the webhook management endpoints are not install-callable yet. Today those routes admit a signed-in Admin or the account's own API key, not an OAuth-authorized install — requestingwebhooks:managewill be accepted at consent time, but calls to/webhooks/*with that token will still403. Provision webhook endpoints with an API key or an Admin session for now; ask your BankConnector contact for the current status if your add-on needs to self-serve this.