A Home Assistant custom integration that shows account balances from any bank supported by Enable Banking > including ASN Bank, N26, Revolut, Openbank, and hundreds more.
Each bank connection is a separate config entry, so you can add as many as you like and see all balances on one dashboard.
The integration uses Enable Banking as the licensed TPP (Third Party Provider). Enable Banking offers a free personal tier that covers a single PSU polling up to four times a day, which is exactly what PSD2 allows for unattended Account Information access.
- One config entry per bank > add ASN Bank, N26, Revolut, Openbank independently
- Balance sensor per discovered account under each entry
- Revolut Business supported: select ASPSP "Revolut" with account type "Business"
- Unit follows the account currency (EUR shows as
€, others as their ISO code such asNOKorSEK), withstate_class: total,device_class: monetary enablebanking.get_transactionsaction: fetch an account's transactions for a date range on demand- Attributes per sensor: IBAN, account name, product, currency, balance type, reference date, bank name,
last_polled_at,last_error,stale,consent_expires_at,consent_days_remaining - Scheduled polling at fixed local times (10:00, 14:00, 18:00, 22:00) with per-entry minute jitter > exactly four polls/day, aligned with PSD2's cap
- Never-unavailable sensors: balances are cached to disk and displayed even during rate-limits, network blips, consent expiry, or the first moment after an HA restart before the first poll runs
- Graceful 180-day consent expiry: proactive 14-day warning, renew early with Reconfigure, automatic reauth UI when the consent lapses
- Self-renewing API token: the integration stores your application private key and signs a fresh 23-hour JWT itself, so there is no token to paste and nothing that silently expires
- Reauth flow that revalidates the existing bank session first, so an expired token is a one-click fix with no bank round-trip
- Home Assistant 2026.4 or newer
- An Enable Banking account (free personal tier)
- Your Enable Banking application's private key (.pem) and application ID
- One active Enable Banking session per bank (valid 180 days)
You only need to do this once. The same private key and application ID serve every bank you add.
- Sign up at enablebanking.com and open the Control Panel.
- Go to API applications > Register a new application. Give it any name (e.g. Home Assistant) and add
https://enablebanking.com/as a redirect URL. - Download the application's private key (.pem) and note the application ID (a UUID).
That is all. You do not generate a JWT by hand: the config flow takes the private key plus the application ID and mints the RS256 token itself, then renews it every 23 hours (Enable Banking caps token lifetime at 24 h). Nothing to re-paste, ever.
The private key is stored in the config entry, so it lives in .storage/core.config_entries on your HA instance in plain text, like every other credential HA holds. Treat a backup of that directory accordingly.
scripts/generate_jwt.py is still in the repo, but only as a debugging aid for poking the Enable Banking API with curl outside Home Assistant. It is not part of the setup flow.
A full walkthrough lives at enablebanking.com/docs/api/quick-start/.
Or manually in HACS:
- HACS > three-dot menu > Custom repositories.
- Add
https://github.com/SurfHost/ha-enablebankingwith category Integration. - Search for Enable Banking and install it.
- Restart Home Assistant.
- Copy
custom_components/enablebanking/into your HAconfig/custom_components/directory. - Restart Home Assistant.
Or: Settings > Devices & Services > Add Integration > Enable Banking.
The config flow has four steps:
- Credentials > paste the full contents of your application's private key (.pem) into the multi-line field and enter the application ID. The flow validates them by minting a JWT and fetching the bank list. On a second or later bank both fields come pre-filled from the entry you already have.
- Country > pick the country the bank is in. This just filters the (long) bank list.
- Bank > pick a bank from the dropdown (populated live from Enable Banking's ASPSP list) and select Personal or Business.
- Authorise > the flow shows a link to your bank's login page. Click it, log in, and you'll be redirected to
enablebanking.com?code=.... Copy thecodevalue from the URL bar and paste it back in HA. The integration exchanges it for a session and creates the balance sensors. A link is valid for about an hour: if the bank shows an error, or you land onenablebanking.com?error=...instead, submit the form with the field empty to get a new link.
Repeat from the top to add more banks.
Select ASPSP Revolut and account type Business. Enable Banking uses a single "Revolut" ASPSP entry with a psu_type field distinguishing personal and business > not two separate entries.
| Property | Value |
|---|---|
| State | Balance (interim available ITAV preferred, falls back to closing booked CLBD, then XPCD, ITBD, OPBD) |
| Unit | Account currency: € for euro accounts, otherwise the ISO code (NOK, SEK, GBP, ...) |
| State class | total |
| Device class | monetary |
Attributes
| Key | Description |
|---|---|
iban |
Account IBAN |
account_name |
Account name or product |
product |
Product type from the bank |
currency |
ISO currency code |
balance_type |
Balance type code (CLBD, ITAV, …) |
reference_date |
Date of the reported balance |
aspsp |
Bank name (useful for templates across multiple entries) |
last_polled_at |
ISO timestamp of the last successful fetch for this account |
last_error |
Short tag if the most recent poll attempt failed: rate_limited, network, consent_expired, auth, api. Empty string on success. |
stale |
Boolean, true when last_polled_at is older than 16 h (twice the 8 h nominal interval; the schedule itself is fixed, see below) |
consent_expires_at |
ISO timestamp when the PSD2 consent expires |
consent_days_remaining |
Integer days until expiry |
The integration polls at four fixed local times per day:
10:00 14:00 18:00 22:00
Per-entry minute jitter (deterministic from entry_id) staggers banks so they don't hit at HH:00:00 simultaneously. No interval setting > times are hard-coded, which guarantees you sit exactly at the PSD2 4/day cap regardless of HA restart frequency.
If HA is down when a scheduled time passes, the coordinator runs one catch-up poll on startup (with 0-60 s jitter). If HA is up but the cache is still within the current schedule window, no startup poll runs at all.
The integration registers one service, enablebanking.refresh, which polls every configured entry immediately:
action: enablebanking.refreshIt is meant for debugging a freshly added bank. Each call spends real PSD2 quota, so use it sparingly.
enablebanking.get_transactions returns the transactions of one account as an action response. It does not create entities and never runs on its own, so it costs nothing until you call it.
| Field | Required | Notes |
|---|---|---|
entity_id |
yes | The account's balance sensor |
date_from |
no | Defaults to 30 days ago. Cannot be in the future (Enable Banking rejects it) |
date_to |
no | May be in the future to ask for scheduled payments, if your bank reports them |
status |
no | BOOK, PDNG, SCHD, HOLD, CNCL, RJCT or OTHR; not every bank supports the filter |
The response has count, total (sum of all amounts) and a transactions list. Each transaction has date, booking_date, value_date, amount (negative for money going out), currency, status, counterparty, description and the bank's untouched payload under raw.
Example: check every month whether the scheduled payments of the next 30 days are covered by the balance.
actions:
- action: enablebanking.get_transactions
data:
entity_id: sensor.no3812345678901
date_from: "{{ now().date() }}"
date_to: "{{ (now() + timedelta(days=30)).date() }}"
response_variable: tx
- variables:
outgoing: >-
{{ tx.transactions | selectattr('amount', 'number')
| selectattr('amount', 'lt', 0)
| map(attribute='amount') | sum | abs }}
- if: "{{ outgoing > states('sensor.no3812345678901') | float(0) }}"
then:
- action: notify.notify
data:
message: "Upcoming payments {{ outgoing }} exceed the balance."Whether future payments show up depends on the bank: PSD2 only obliges banks to report booked and pending transactions, and many leave scheduled ones out. Each call spends the account's PSD2 quota for transactions (usually 4 per day per account, separate from the balance quota).
A multi-bank view using one tile per account, grouped by bank:
type: vertical-stack
cards:
- type: heading
heading: ASN Bank
- type: tile
entity: sensor.asn_bank_balance_nl00asnb0123456789
name: Betaalrekening
icon: mdi:bank
color: green
- type: heading
heading: N26
- type: tile
entity: sensor.n26_balance_de00n260987654321
name: Current Account
icon: mdi:credit-card
- type: heading
heading: Revolut (business)
- type: tile
entity: sensor.revolut_balance_lt000000000000000000
name: Business Account
icon: mdi:briefcase
- type: heading
heading: Openbank
- type: tile
entity: sensor.openbank_balance_es00open0000000000
name: Cuenta Corriente
icon: mdi:bank-outlineReplace entity IDs with the ones Home Assistant created for your accounts. A template card showing days until consent expiry:
type: markdown
content: >-
{% set s = states.sensor | selectattr('attributes.consent_expires_at', 'defined') | list %}
{% for e in s %}
**{{ e.attributes.aspsp }}**: {{ e.attributes.consent_days_remaining }} days remaining
{% endfor %}PSD2 caps unattended Account Information polling at 4 times per day per consent. Every HA restart, reload, or manual reconfigure burns one of those slots. If you exceed it the bank responds with HTTP 429 / ASPSP_RATE_LIMIT_EXCEEDED (HUB046 on de Volksbank's API) and refuses further polls until the rolling 24 h window elapses.
Instead of a configurable interval, the integration polls at four fixed local times > 10:00, 14:00, 18:00, 22:00 > hitting the 4/day cap exactly and predictably. No restart can burn extra quota because the cache supplies startup values.
- Balances persist across HA restarts. The last successful balance per account is written to
.storage/enablebanking.<entry_id>.cache. On startup the sensor shows the cached value immediately > no API call is made. - Skip the boot-time poll. If the cache still sits within the current schedule window, the first post-restart poll is skipped > the next scheduled slot handles it. Catch-up only runs if HA was down during a scheduled slot.
- Staggered startup. The catch-up (when it does run) is jittered 0-60 s per entry, so four banks don't all burst at the same second.
- Per-account back-off. If a single account returns 429, that account's next scheduled slot is skipped entirely, then normal cadence resumes. Other accounts under the same bank keep polling.
- Sensors never go
unavailable. On any failure (rate limit, network, consent expiry, API error) the sensor keeps displaying the last known balance. Thelast_errorattribute tells you why the latest attempt failed; thestaleattribute flips totrueonce the cache is older than 16 hours. - Self-renewing token. The stored private key lets the coordinator mint a new JWT whenever the current one is within 30 minutes of expiry, so an expired token never costs you a poll or a manual step.
- Smart reauth. If reauth is triggered while the bank session is in fact still alive, the flow mints a token, revalidates that session and finishes without a bank round-trip. Only a genuinely dead session sends you back to the bank's login page.
PSD2 limits unattended Account Information consent to 180 days, after which the user must re-authorise (Strong Customer Authentication) regardless of how frequently they have polled.
- 14 days before expiry: a
persistent_notificationappears in HA with the bank name and days remaining, prompting you to renew in advance. It disappears by itself once the consent is renewed. - On expiry (or if the bank revokes consent early): the next poll receives a session-not-found response. The integration triggers the reauth flow (the standard "Integration needs attention" card appears under Notifications) and sets
last_error: "consent_expired"on the sensors. - Sensors keep showing the last known balance with
stale: true. They do not go unavailable. The reauth card does the nagging.
Before it expires (after the 14-day warning, or whenever you like): go to Settings > Devices & Services > Enable Banking (the link in the notification opens it), click the ⋮ menu next to your bank and choose Reconfigure (Herconfigureer in Dutch). The integration uses your stored private key and application ID and goes straight to a new bank link; complete the bank authorisation (step 4 of the setup flow above) and the entry switches to the new consent. Until you finish, the current consent keeps working.
After it expired: the entry appears under Attention required (Aandacht vereist) at the top of Settings > Devices & Services. Its Reconfigure button starts the reauth flow, which pre-fills your private key and application ID and, if the bank session really is gone, asks you to complete a fresh bank authorisation.
You do not need to regenerate your application private key unless you revoked it at Enable Banking. The JWT is never your problem: the integration signs and renews it on its own.
| Symptom | Likely cause |
|---|---|
| "Credentials rejected" at step 1 | Private key pasted partially (the -----BEGIN/-----END lines must be included) or the application ID belongs to a different key |
| "Auth code rejected" at step 4 | Copied the wrong query parameter, use only the code= value |
Bank link lands on enablebanking.com/?state=...&error=invalid_request |
The link has expired (links are valid for about an hour) or was already used on an earlier attempt that failed at the bank. Submit the form with the field empty to get a new link (during reauth, closing and reopening the dialog also does it) |
| Entry loads but no sensors appear | The consent granted no accounts. The log says so at WARNING level; check in the Enable Banking dashboard that the account is linked and the app's Account Information service is not restricted |
Sensor shows unavailable |
Consent expired or bank revoked access, use the Reconfigure button under Attention required in Devices & Services |
| No Reconfigure in the ⋮ menu | You are on 0.6.12 or older; renewing before expiry needs 0.6.13 or later |
| Balance stuck / not updating | Check HA log at logger: custom_components.enablebanking: debug |
MIT