sandbox.bankconnector.com API Reference

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 in Signing in & 2FA.

BankConnector has two credentials:

CredentialHeaderPlaneWhat it's for
API keyX-API-Key: key_…data planesubmitting payments, reading journal / reconciliation / statements, webhooks
Admin sessionX-Session-Token: …control planecreating/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. It is a different, narrower credential the customer authorizes and can revoke on their own.

An API key cannot create admin users or set up connections. Those are admin-session actions and return 403.

If you provisioned a sandbox (POST /api/sandbox/provision, then /verify with the emailed code), the response hands you the apiKey only. For an admin session, sign up at /signup with an email you verify. (Sandbox tenants and their credentials are deleted after 30 days of inactivity.)


The API key (data plane)

Your integration's credential, sent as a single header:

X-API-Key: key_<64 hex characters>
  • The key is bound to one company (the organisation it was issued for) and is key_ followed by 64 hex characters.
  • There is no tenant header. You pass companyId, in the body or query, and it must be the key's own company. A request naming any other company 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.
  • The key is shown once, at creation, and cannot be recovered. If you lose it, rotate.

A key cannot reach another company's data, even one you also administer. An integration that serves several companies holds one key per company.

What an API key cannot do

API keys drive machine tasks: payments, webhooks, exports. They get 403 on control-plane routes that need an authenticated user:

  • Naming a bank account (PATCH /api/accounts/<id>)
  • 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 invite viewer users (POST /api/users), remove roles, and grant viewer, so an ERP can give its own staff read access without a User in the dashboard. But a key is deliberately capped so it can't escalate: it cannot create or grant admin or approver (either needs an admin's two-factor code), change an admin's roles, 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.

To do those, an admin logs in with a session: Signing in & 2FA.

An admin of the company mints the API key, and only its metadata is listable afterwards. A provisioned sandbox returns the key in the POST /api/sandbox/provision/verify response.


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. CSRF applies only to cookie-based browser sessions.

Optional headers worth knowing

HeaderPurpose
BankConnector-Version: YYYY-MM-DDpin the API version (baseline 2026-06-01). Echoed back on every response, including errors. A malformed date is a 400.
X-Request-IDset by the server on every response. Log it; support can look up any request by it.
Idempotency-Replayed: trueresponse header telling you a payment was a replay, not a fresh execution.

Auth failures

SituationStatuscode
Missing / invalid credential401unauthorized
Credential valid but wrong scope (e.g. a companyId other than the key's own company)403forbidden
API key on most control-plane routes (e.g. PATCH /api/accounts/<id>)403forbidden
API key on the bank-connection routes (/connections, /connections/<id>/…)403admin_required
API key on the approval-policy mutation routes (POST /approvals/policies, .../<id>/…)403admin_required
API key on a session-only identity route (/auth/me, /me/sessions, /2fa/*)401unauthorized

⚠️ Don't branch on admin_required alone. Some control-plane denials come back as plain 403 forbidden, and routes that require an admin session (bank connections, approval-policy mutations, user administration, among others) answer 403 admin_required instead; this table gives examples, not a full list. 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 this request will not succeed as written: switch to an admin session.


Application installs (OAuth 2.0, for add-on developers)

Audience: add-on / ERP-plugin developers. If you run 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. A stock OAuth 2.0 client library works.

The flow is standard (RFC 6749 §4.1, RFC 7636 PKCE, RFC 8252 for native apps). Read the scope section below: it decides 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 enforces. scopes_supported lists what an application install may be granted; identity_scopes_supported lists what a person may carry into an ERP add-on on their own sign-in. Configure your library from it rather than hardcoding the endpoint URLs below.

Its issuer and endpoint URLs are the deployment's own public origin. Fetch it from the origin you intend to use and, as RFC 8414 §3.3 requires, refuse a document whose issuer is not that origin.

The flow

  1. GET /app/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. A desktop add-on is a public native-app client with no client secret: it authenticates by proving it holds the code_verifier matching the code_challenge.
  2. 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; localhost is never accepted) carrying a single-use authorization code (10-minute TTL) and your state. A web or mobile app registers exact redirect URIs instead; see From a web or mobile app.
  3. POST /api/oauth/token (application/x-www-form-urlencoded) exchanges the code + code_verifier for a token pair:
  4. access_token (bc_oat_…): 1 hour lifetime, sent as Authorization: Bearer <access_token> against the product API exactly like an API key.
  5. refresh_token (bc_ort_…): 180 days, and rotates on every use: grant_type=refresh_token consumes the presented token and mints a new pair. The just-consumed token can be retried for 30 seconds, as long as the pair it produced hasn't been used yet, so a client that crashes (or loses the response) before saving the rotation can retry and gets a fresh pair. Replaying it after that window, or once the new pair is in use, is treated as theft and revokes the entire token family. The user then has to re-consent.
  6. POST /api/oauth/revoke (RFC 7009) revokes a token your add-on no longer needs (on uninstall, for example). It always answers 200, including for an already-revoked, expired, or unknown token (RFC 7009 §2.2). Revoking the refresh token also ends the install it belongs to: the customer's Connected apps screen then lists it as Disconnected. Revoke the old refresh token when a new connection replaces it, and when the user disconnects.

⚠️ /api/oauth/token and /api/oauth/revoke do not use this API's usual error envelope (see Errors & retries). They answer the flat RFC 6749 §5.2 shape, { "error": "invalid_grant", "error_description": "…" }. Don't reuse your error.code handling here.

The device flow (RFC 8628) is not supported: /api/oauth/token refuses the device grant. Use the authorization code flow above.

Scopes: what your add-on can reach

An install's authority is its grant: the set of scopes the authorizing Admin approved. It is 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:

ScopeCovers
payments:submitSubmitting payments (POST /journal/payments and previews).
payments:readReading journal documents / reconciliation data.
statements:readReading account statements and balances.
engine:useThe stateless conversion/validation engine (/convert, /validate, bank + payment-type discovery).
banks:readReading bank connectivity/profile metadata.
webhooks:manageRegistering and managing webhook endpoints.
users:readReading the user list (GET /users) with each person's approverBanks; never inviting or re-roling.
onboarding:readReading setup progress (GET /onboarding/status) and the partner (GET /onboarding/partner).
partner:proposeProposing the partner's Uniconta number (POST /onboarding/partner/uniconta-pid-proposals); sets nothing.

Request only what your add-on needs. The consent screen shows the Admin this list by name.

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. So a compromised add-on cannot redirect where payments go or weaken the approval control. The one path to an approval decision inside an add-on is a person's own sign-in, never the install; see identity grants below.

⚠️ webhooks:manage is 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. Requesting webhooks:manage is accepted at consent time, but calls to /webhooks/* with that token will still 403. 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.

A person's own sign-in inside the add-on (identity grants)

Everything above is the install: a machine actor, authorized by an Admin for the organisation, that can never approve a payment. Separately, a person can sign in to your add-on as themselves, so they can approve inside the ERP (or, as an Admin, invite colleagues or follow setup) without switching to the browser.

  • Request it through the same GET /app/authorize → POST /api/oauth/token flow, with a scope made only of identity scopes. A request that mixes identity scopes with the install scopes above is refused (invalid_scope), except from the Uniconta add-on, which may ask for both in one consent: the loopback then carries identity_code beside code, or error_reason when the person may not hold the sign-in, and each code redeems with the same code_verifier. Your application must be registered for the identity scopes it asks for.
Identity scopeWho may hold itCovers
approvals:readan approvertheir own queue (GET /approvals/pending) and GET /approvals/{id}
approvals:decidean approverPOST /approvals/{id}/approve and /reject
users:invitean AdminPOST /users: invite-only, non-admin users (approver or viewer)
setup:readan AdminGET /onboarding/status (see Guiding setup)
  • The person consents for themselves, in the browser, with a fresh two-factor code. Nobody can grant it on their behalf. The person sees and ends their own add-on sign-ins in their settings (GET /me/erp-grants, POST /me/erp-grants/{grantId}/revoke), and an Admin sees and revokes anyone's with GET /users/{id}/erp-grants and POST /users/{id}/erp-grants/{grantId}/revoke. A sign-in also ends when the person's password is changed or reset, or their two-factor authentication is turned off or reset; the add-on then signs them in again.
  • The organisation must switch it on. An Admin turns it on in the browser (PATCH /company/erp-approval, read with GET /company/erp-approval). It is off by default; for an organisation that signed up from an ERP add-on it turns on by itself once both preconditions below hold, unless an Admin has set it first. Turning it on is refused (409) unless the organisation requires two-factor authentication (erp_approval_requires_two_factor) and every active approval policy names its approvers (erp_approval_requires_named_approvers). Until then the consent screen refuses identity scopes (403 erp_approval_unavailable). A grant holding only setup:read is exempt from these preconditions.
  • Every decision is checked again. GET /approvals/{id} returns the request's payment lines, totals and a contentDigest. The approve or reject call must echo that contentDigest and carry a fresh two-factor code (code, or backupCode) every time. A digest that differs from the current content is 409 content_changed; a request the add-on may not decide (for example a batch above the organisation's optional maxBatchAmount ceiling) is 403 decide_in_browser, and the approver decides it in the browser instead. GET /approvals/{id} answers 404 for a request that does not name the signed-in person as an approver.
  • Nothing about what may be released changes: the approval policy, its approvers, limits, ordered tiers and the two-person minimum all apply exactly as in the browser. Each invite from the add-on needs the Admin's fresh code and is emailed to every Admin in the organisation; an invited approver can approve nothing until an Admin names them in a policy in the browser.

From a web or mobile app

The same identity grant works from a platform's own web app or phone app, so a person can approve payments there. The flow, the scopes and every rule above are the same; only the redirect and the client's authentication differ.

  • Registration. BankConnector registers each app as its own application (its own client_id), with the exact redirect URIs it may use and the identity scopes it may ask for. Register the web app and the mobile app separately: a person has one sign-in per application, and a new sign-in to the same application ends their earlier one.
  • Web app. The redirect is an https URL you registered, for example https://app.example.com/oauth/callback. It must match exactly: the same host, path and port, with no query or fragment. A web app may also be given a client secret (bc_ocs_…, shown once). Keep it on your server. A client with a secret must send it on every POST /api/oauth/token and POST /api/oauth/revoke, either as HTTP Basic (Authorization: Basic base64(client_id:client_secret)) or as client_secret in the form, but not both. A missing or wrong secret is 401 invalid_client. PKCE is still required.
  • Mobile app. Open the authorize URL in the system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android), never an embedded web view (RFC 8252). The redirect is either an https link your app has claimed (a universal link or app link) or a private-use scheme in reverse-domain form with one slash, such as com.example.app:/oauth2redirect (RFC 8252 §7.1). A mobile app gets no client secret, because one shipped inside an app can be extracted. PKCE alone authenticates it, and a request that sends a secret is refused.
  • The person signs in to BankConnector in that browser if they are not already signed in, and approves the sign-in with a fresh two-factor code. The browser then goes to your redirect with code and your state. Check state before you redeem the code.

Guiding setup from an add-on

An add-on's setup wizard can send the customer to the browser-only steps and follow their progress:

  • Signup, pre-filled. Open /signup with the values in the URL fragment (after #, never the query string): companyName, registrationNumber, country, adminName, adminEmail and source=erp:<slug>. Every value stays editable and is validated as if typed. source is recorded for attribution only; a malformed one is refused with 422.
  • Deep links into the app land on the step: /app/banks/<bankKey>, /app/policy/new, /app/policy/edit/<policyId> (#add=<userId> preselects a person to name) and /app/company. Add #erp=<slug> and the page says where the Admin came from and when they can return to the ERP. Add &org=<organisation id> (organisation.id from GET /oauth/userinfo) and the link acts only in that organisation: a browser signed in to another one fills nothing in and is told so.
  • Progress. GET /onboarding/status returns what the organisation has set up: whether it requires two-factor authentication and has approving in the ERP on, each bank connection's bank key, name and status, each active approval policy and whether it names its approvers, and each user's name, email, roles, status and two-factor enrolment. It carries states, never secrets. It answers an Admin's own add-on sign-in holding setup:read, the add-on's company connection holding onboarding:read, or an Admin in the browser; an install without that scope, an API key and a non-Admin get 403.

Your add-on's version

Name your add-on's version as the first product token of the User-Agent on every request: User-Agent: <product>/<major>.<minor>.<patch>, for example BankConnector-Uniconta/1.4.2. Three whole numbers; anything else counts as no version.

BankConnector can set a minimum version for your application, below which it stops serving the add-on. A request authenticated by your application's access token that names an older version, or none, is then refused with 403 addon_update_required. Its message tells the user how to update, and details carries clientVersion (null when the request named none) and minimumVersion. Show the message and stop; retrying or signing in again gets the same answer until the add-on is updated. While your application has no minimum, every version is served, including a request that names none.