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
- Sign in to the Enable Banking Control Panel (an account is created on first sign-in; it is self-serve).
- 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.
- 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. - 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.
- Registering downloads the application's private key, a
.pemfile named after the application id. Set both as environment variables of the web app and the worker:
| variable | what goes in it |
|---|---|
ENABLE_BANKING_APP_ID | the application id (the .pem file's name, without .pem) |
ENABLE_BANKING_PRIVATE_KEY | the 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.
Checking it before the first link
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:
| check | what it asks | what to fix when it fails |
|---|---|---|
credential | both variables are set and the key is an RSA private key | set them; paste the whole .pem |
bearer | Enable Banking accepts a bearer signed with the key, for this application id | the key and the id are from two different applications, or the application is not active |
read_only | the application holds account information and, outside the sandbox, not payment initiation | register a production application for account information only |
redirect_url | this deployment's callback URL is one the application registered | add the URL it prints in the Control Panel |
banks | the bank list answers for --country (default FI), naming --bank when given | pick 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.
The first real link
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:
- Register a Sandbox application (above) with this deployment's callback URL, and set both variables on the web app and the worker.
- Run
botseon bank-link check --bank "Mock ASPSP"until it exits 0. - 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. - Ask a bot for the balance of one account: that is
enable-banking.get_balancesover the link. - Copy the account's
provider_account_idinto theconnector-liveenvironment asENABLE_BANKING_TEST_SESSION_ID(below), so the nightly run reads it from then on. - 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:
| name | kind | what it is |
|---|---|---|
ENABLE_BANKING_APP_ID | variable | a sandbox application's id |
ENABLE_BANKING_PRIVATE_KEY | secret | that application's private key |
ENABLE_BANKING_TEST_SESSION_ID | variable | a consent a person completed once at the test bank (below) |
ENABLE_BANKING_TEST_BANK | variable | the test bank's name; unset means Mock ASPSP |
ENABLE_BANKING_TEST_COUNTRY | variable | its 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
- 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.
- 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.
- 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.
- 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 inbank_consent_accountsbeside 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.
When a consent lapses
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
readand 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,deleteorexternal_send, and no tool is named anything containingpay,transfer,initiateormandate; - 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.