Bank, card and investment accounts: Enable Banking

Operator·11 minutes to read

This connector reads a member's bank, card and investment accounts read-only, through Enable Banking, an EU open-banking aggregator, under PSD2 account information (AIS). It cannot move money, and there is no tool on it that could — see What it cannot do below, which is the part of this page that is enforced by tests rather than by this page.

It does not fit the "bring your own OAuth app" shape most pages beside this one describe, and it does not fit the paste-a-key shape either. It is a third thing:

The deployment holds the credential; the member holds the consent. Your deployment registers one Enable Banking application. What the member's authorisation produces is not a token but a link: Enable Banking's session id, naming the consent they gave their own bank. So the account's auth_kind is hosted_consent.

What you set up, once per deployment

  1. Sign in to the Enable Banking Control Panel (an account is created on first sign-in; it is self-serve).
  2. Add a new application. Choose the environment: Sandbox to try it with the sandbox bank, Mock ASPSP, which needs no bank credentials; Production for real banks. A production application starts restricted and reads only the accounts linked to it with Activate by linking accounts until you sign Enable Banking's agreement.
  3. Give it your deployment's callback URL as its redirect URL — exactly https://<your public URL>/api/oauth/enable-banking/callback (finding your redirect URL). Enable Banking only sends a member back to a redirect URL the application registered.
  4. Register a production application for account information only. Before every link Botseon asks Enable Banking which services the application holds, and refuses to start a link if a production application can also initiate payments. A sandbox application is the exception: Enable Banking switches payment initiation on for every new one, and the sandbox moves no real money, so Botseon accepts it there.
  5. Registering downloads the application's private key, a .pem file named after the application id. Set both as environment variables of the web app and the worker:
variablewhat goes in it
ENABLE_BANKING_APP_IDthe application id (the .pem file's name, without .pem)
ENABLE_BANKING_PRIVATE_KEYthe contents of the .pem file. A secret store that holds one line may hold it with \n in place of each line break; both are read

There is no per-organisation row: every organisation on the deployment links through the same application. The tile is in every member's catalogue (the seed ships it available since S-96814); with either variable unset, Authorise cannot start and the bank picker says bank linking is not available yet. The private key never leaves the process: each request is signed with a short-lived token (one hour) made from it, and nothing derived from it is stored.

Every request goes to https://api.enablebanking.com through the one vetted egress client, so it is subject to the same address, redirect, size and timeout rules as every other provider call.

Once both variables are set, ask the vendor whether a member's link can start, before any member tries — from the deployment's own environment, so it reads the same ENABLE_BANKING_APP_ID, ENABLE_BANKING_PRIVATE_KEY and BOTSEON_PUBLIC_URL the web app does:

pnpm --filter @botseon/cli exec tsx src/main.ts bank-link check --country FI --bank "Mock ASPSP"

botseon bank-link check makes the calls a link makes, in the same order, and starts no consent at any bank. It prints one line per check and exits 0 only when all five pass:

checkwhat it askswhat to fix when it fails
credentialboth variables are set and the key is an RSA private keyset them; paste the whole .pem
bearerEnable Banking accepts a bearer signed with the key, for this application idthe key and the id are from two different applications, or the application is not active
read_onlythe application holds account information and, outside the sandbox, not payment initiationregister a production application for account information only
redirect_urlthis deployment's callback URL is one the application registeredadd the URL it prints in the Control Panel
banksthe bank list answers for --country (default FI), naming --bank when givenpick a country the application was registered for

A check that depends on a failed one prints - and is not run, so the first ✗ is the one to act on. Nothing it prints carries the key or a bearer.

Main ships the whole link; what a deployment adds is the application. To take a deployment from "Bank linking is not available here yet." on the bank picker to a linked bank:

  1. Register a Sandbox application (above) with this deployment's callback URL, and set both variables on the web app and the worker.
  2. Run botseon bank-link check --bank "Mock ASPSP" until it exits 0.
  3. Sign in as a member, add Enable Banking from the catalogue, pick Finland and Mock ASPSP, and press Authorise. Complete Mock ASPSP's page (it asks for no bank credentials) and choose the accounts. The account reads Connected.
  4. Ask a bot for the balance of one account: that is enable-banking.get_balances over the link.
  5. Copy the account's provider_account_id into the connector-live environment as ENABLE_BANKING_TEST_SESSION_ID (below), so the nightly run reads it from then on.
  6. For real banks, repeat with a Production application; until Enable Banking's agreement is signed, it reads only the accounts linked to it in the Control Panel.

Checking it against the sandbox

packages/connectors/src/enable-banking.live.test.ts runs against Enable Banking's real API, nightly in the connector-live workflow. Every case skips without the two variables above in the GitHub environment connector-live:

namekindwhat it is
ENABLE_BANKING_APP_IDvariablea sandbox application's id
ENABLE_BANKING_PRIVATE_KEYsecretthat application's private key
ENABLE_BANKING_TEST_SESSION_IDvariablea consent a person completed once at the test bank (below)
ENABLE_BANKING_TEST_BANKvariablethe test bank's name; unset means Mock ASPSP
ENABLE_BANKING_TEST_COUNTRYvariableits country; unset means FI

Each is stored in the environment with gh. The secret takes no --body: gh asks for the value, or reads it from stdin, so the key never lands in shell history. Then run the workflow on main:

gh secret set ENABLE_BANKING_PRIVATE_KEY --env connector-live --repo botseon/cloud < <application id>.pem
gh variable set ENABLE_BANKING_APP_ID --env connector-live --repo botseon/cloud --body '<application id>'
gh variable set ENABLE_BANKING_TEST_SESSION_ID --env connector-live --repo botseon/cloud --body '<session id>'
gh variable set ENABLE_BANKING_TEST_BANK --env connector-live --repo botseon/cloud --body 'Mock ASPSP'
gh variable set ENABLE_BANKING_TEST_COUNTRY --env connector-live --repo botseon/cloud --body 'FI'
gh workflow run connector-live.yml --ref main --repo botseon/cloud

The job summary's enable-banking row reads proved only when every case ran and passed; with the session id unset it reads "no test account for N of M cases". The names here, the workflow's and the live test's are held equal by packages/connectors/src/enable-banking-live-wiring.test.ts.

With the first two, the nightly run runs botseon bank-link check's own checks against the application, checks the signed token is accepted, that the application holds account information and not payment initiation, that the bank list answers, and — when the application's registered redirect URL is this product's callback path — that beginAuthorisation gets a real bank page for the test bank. With the session id it also reads accounts, balances, transactions and details through the driver. The operator's checklist for the test application, the five names and the one sandbox consent is connector-live-setup/enable-banking.md.

A bank consent is given by a person in a browser, so completing a link and deleting a completed one do not run in CI. To make the session id, link the sandbox bank once from a deployment pointed at the sandbox application, then read the session id from the account's provider_account_id.

What the member does

  1. On the plugin sheet, before anything leaves the browser, the sheet names the three things the consent covers: the accounts (current, card and investment), what is read (each account's name and masked number, its balance, its transactions), and the date the consent lapses.
  2. The member picks their bank: a country, then their bank from Enable Banking's own list for that country, then personal or business online banking where the bank offers both.
  3. Authorise opens the bank's own page. The member completes their bank's strong authentication there and chooses the accounts to share. No bank credential ever reaches this deployment.
  4. They come back to the same callback URL every connector returns through, and the account reads Connected. Under it, Linked accounts lists each account they chose, by the bank's own name for it and its kind (current account, card or investment account) — what the bank listed when the consent was completed, kept in bank_consent_accounts beside the link, with no IBAN, balance or holder name.

A bot's read names one of those accounts. A call naming any other account id is refused before a request is made — that account is not one this bank consent covers — so a bot cannot reach an account the member did not choose, even one the same bank holds for them. A link made before the list was kept covers nothing it can name until the member reconnects it.

A PSD2 account-information consent is a credential with a clock on it, and it is not refreshable. Botseon asks for 90 days and keeps whichever is sooner of that and the date the bank actually granted. When it lapses the account moves to Expired, every tool is withheld, and the sheet asks the member to Reconnect: a new consent at their bank. The member is asked before the next read, not after a failed one.

Disconnecting

Disconnect bank deletes the session at Enable Banking first, which ends the consent at the member's bank, and only then clears anything here — the link, and the list of accounts it covered. That order is the opposite of the OAuth connectors' and it is deliberate: a bank consent is live at the member's own bank and outlives every row in this deployment, so the thing that must happen first is the one this deployment cannot do later.

If the delete at Enable Banking does not succeed, the member is told so plainly — the link was removed here, the consent may still stand at their bank, and it is withdrawn in the bank's own app or online banking. The account is disconnected locally either way: nobody is left holding a connection they cannot remove.

What it cannot do

Four tools, every one read-only:

  • enable-banking.list_accounts — the accounts the consent covers, with the bank, the kind and the currency.
  • enable-banking.get_balances — one account's balances.
  • enable-banking.list_transactions — one account's booked and pending entries, debits negative.
  • enable-banking.get_account_details — one account's holder name, masked identifier, product and currency.

There is no fifth. Read-only here is the absence of a tool, not a flag you could turn off:

  • every tool is tagged read and needs the read half of the scope set;
  • the write half of the scope set is empty, so a write grant unlocks nothing;
  • no tool is tagged spend, delete or external_send, and no tool is named anything containing pay, transfer, initiate or mandate;
  • the driver builds no request but a GET, and the client addresses no payment path at all;
  • and one level up, the application must hold account information and not payment initiation: Enable Banking's API has a payments half, so Botseon reads the application's own services back before every link and refuses a production one that could reach it. (The vendor's sandbox turns payments on for every application and moves no real money, so a sandbox application is let through; the driver still never calls a payment path.)

Payment initiation is a separately licensed activity this product does not perform.

Residency

Under an eu_only residency policy the aggregator must declare an EU (or adequacy-covered) contracting entity and a transfer mechanism (eea, adequacy or scc), or the link is refused — with a reason naming the entity and the country, before anything is created at the vendor. Enable Banking's row declares Enable Banking Oy, Finland, which every policy admits. eu_first needs only a declared mechanism, so it admits a transfer under standard contractual clauses from any entity; any admits every aggregator that cannot initiate payments. The research page is where the claim is confirmed or replaced.

Last verified against build 2012031.