DEV Community

Building a Personal Finance App with Open Banking APIs in 2026 (No Certificates Required)

Building a personal finance app used to mean begging users to paste a CSV export - or worse, to share their banking password with a screen-scraping service. Open Banking was supposed to fix that, and under PSD2 it largely has. Every regulated EU/UK bank now exposes a standard API that returns your balances and transactions as clean JSON, with your explicit consent. So why isn't every indie developer shipping a net-worth dashboard on top of it? Because the "official" path to those APIs requires an eIDAS qualified certificate (QWAC + QSeal), a regulator licence, and weeks of compliance paperwork. That's the wall most tutorials quietly skip. This one doesn't. We'll cover how PSD2 APIs actually work, why the certificate requirement blocks indie builders, and then build a working personal-finance tracker end-to-end using a certificate-free provider - with real curl and Python you can run today. What an "Open Banking API" actually means Two pieces of jargon do most of the heavy lifting, and clearing them up front will save you hours: - PSD2 is the EU directive (second Payment Services Directive) that forced banks to open up customer data to licensed third parties, on the customer's request. The UK's Open Banking standard is the local implementation of the same idea. - AISP / PIS - an Account Information Service reads data (balances, transactions); a Payment Initiation Service can move money. For a personal finance app you only ever need AIS. That's read-only by law. The thing that makes Open Banking safe - and the thing that makes it annoying to set up - is the consent redirect flow. Your app never sees the user's bank password. Instead: - Your app asks the API provider to start a connection for, say, "Nordea" or "Lunar". - The provider redirects the user to their own bank's login page (the bank's domain, the bank's TLS). - The user logs in there and approves a scoped consent (e.g. "read balances and transactions for 90 days"). - The bank redirects back to your app with an authorisation code. - Your app exchanges that code for access/refresh tokens and can now poll the data API. At no point do you touch credentials. The bank hands you a token with a defined scope and an expiry. The certificate wall (and why it blocks indie builders) Here's the part the vendor docs bury. To call a bank's PSD2 API directly, as your own licensed third-party provider (TPP), you must present a QWAC (Qualified Website Authentication Certificate) and a QSeal (Qualified Electronic Seal) - collectively the eIDAS qualified certificates. You get those by: - Becoming an authorised AISP with your national regulator (the FCA in the UK, Finanstilsynet in Denmark, BaFin in Germany, etc.). - Passing audits and maintaining a compliance programme. - Buying the certificates from a qualified trust service provider - typically €1,000-€6,000/year, plus setup. For a bank or a funded fintech, that's Tuesday. For an indie developer who just wants to build a net-worth dashboard over the weekend, it's a non-starter. So either you give up, or you use an aggregator that is already licensed and lets you ride on its authorisation - handing you a simple API key instead of a certificate. That second option is the pragmatic one, and it's what the rest of this tutorial uses. The certificate-free approach A handful of providers are already licensed AISPs and expose their aggregated API to you behind a plain API key. They hold the certificates; you hold a key. You still go through the bank's consent redirect - the security model is identical - but you skip the licensing and certificate procurement entirely. The provider used in the code below is open-banking.io (full disclosure: it's a service I built, which is exactly why I can show you the cheapest path). It's certificate-free, costs €3/month for one account, and connects to EU/UK banks. The pattern - API key, consent flow, JSON data - is the same one you'll see at Yapily, Tink, TrueLayer, or Enable Banking; the difference is the absence of certificates and the price tag. Adapt the code to whichever provider you pick. Now let's build something. Step 0 - Get API access (no certificates, ~2 minutes) - Create an account and connect a real bank through the consent redirect described above. - Generate an API key and export a credentials.json bundle. The bundle pairs your API key with your private decryption key (more on why in the security section). There is no eIDAS step. There is no audit. You authenticate every request with a single header: curl -H "X-Api-Key: $OBIO_API_KEY" https://open-banking.io/api/accounts That's the entire auth model. No OAuth client-secret dance on your side, no mTLS, no certificate rotation. Step 1 - List accounts and balances (Python) Install the Python client (or just use httpx against the raw REST endpoint - your call): pip install open-banking-io Then list every connected account with its booked balance: from open_banking_io import OpenBankingClient with OpenBankingClient.from_credentials("credentials.json") as client: for account in client.get_accounts(): booked = next((b for b in account.balances if b.type == "ITBD"), None) label = account.display_name or account.owner_name print(f"{label} ({account.iban}): {booked.amount} {account.currency}") Run it and you get something like: Drift (DK64 6466…4927): 12482.55 DKK Salary (DK21 3000…8841): 828.13 DKK Buffer (DK90 5301…2207): 34010.00 DKK Two things worth noting. First, balances come in types (ITBD = interim booked, CLBD = closing booked, XPCD = expected, etc.) - always pick the one that matches your use case. "Booked" is the safe default for a net-worth figure because it excludes pending authorisations. Second, amount is a decimal.Decimal , not a float - keep it that way or you'll silently lose pennies to floating-point error. This trips up a lot of finance apps. Step 2 - Pull and paginate transactions def load_txns(client, account_id, page_size=200): txns, offset = [], 0 while True: page = client.get_transactions(account_id, limit=page_size, offset=offset) txns.extend(page.items) if len(page.items) 0: income += n["amount"] else: spend += -n["amount"] print(f"Income: {income:>12,.2f}") print(f"Spending: {spend:>12,.2f}") print(f"Net cashflow: {(income - spend):>12,.2f}\n") print("Top 5 spenders:") for name, amt in sorted(by_counterparty.items(), key=lambda kv: kv[1])[:5]: print(f" {name:30s} {amt:>10,.2f}") That's a real, working spend tracker. From here the usual next steps are obvious: bucket by month, flag recurring subscriptions (same counterparty, same amount, ~30-day cadence), raise an alert when income drops, and project a 30-day balance forecast. None of that needs the bank API at all - you already have clean, signed, normalised data. Step 4 - Schedule a daily sync and cache You don't want to hit the bank on every page load. The standard pattern is a daily server-side job that pulls fresh transactions into your own database; your frontend reads from your DB, not the bank. # cron / scheduled job - run once per day with OpenBankingClient.from_credentials("credentials.json") as client: client.sync_all() # refresh every connected account from the bank for acct in client.get_accounts(): for t in load_txns(client, acct.id): upsert_into_db(t) # idempotent insert keyed on the transaction id sync_all() triggers an online refresh; get_transactions() then returns the latest rows. Keep the credentials bundle server-side only (see below) and you have a textbook ETL: bank β†’ provider β†’ your DB β†’ your app. Security: zero-knowledge, and why it matters for a finance app A personal finance app is a high-trust product. Two security properties are worth understanding before you ship: The consent layer is bank-enforced. Read-only is not a setting in your code - it's a legal scope the bank grants at consent time. Your token simply cannot initiate a payment if you only asked for AIS. That's a much stronger guarantee than a "we promise not to move money" checkbox. End-to-end encryption / zero-knowledge. The better providers don't store your data in a form they can read. open-banking.io, for example, encrypts every account and transaction envelope under a key derived from your private key (ECDH P‑256 β†’ HKDF‑SHA256 β†’ AES‑256‑GCM). The server holds ciphertext it cannot decrypt; decryption happens in your client process, using the private key from your credentials.json . If the provider is breached, the attacker gets sealed blobs. Practical implication: the credentials.json bundle contains your private decryption key. Never ship it to a browser, a mobile app, or a public repo. Keep it on your server - ideally as a Kubernetes Secret or a secrets manager, mounted read-only: kubectl create secret generic obio-credentials \ --from-file=credentials.json=./open-banking.io-credentials-2026-08-12.json A word on cost Most aggregators price for enterprise: per-connection fees in the euros, minimum commits, sales calls. That's fine if you're a funded startup, brutal if you're an indie. The certificate-free providers sit at the cheap end of the spectrum - open-banking.io is €3/month for one account and €1/month per additional account, no minimum. When you pick a provider, model your cost per active user, because transaction volume and connection count are where the bill climbs. What you've got At this point you have: - A way to read bank data without an eIDAS certificate or a licence. - A consent flow that's bank-enforced and read-only by law. - Clean, normalised transactions to build any insight on top of. - A daily-sync ETL pattern with zero-knowledge encryption. That's the complete foundation of a personal finance app - net worth, cashflow, subscription tracking, the lot - built on PSD2 APIs without touching the certificate wall. The hard part of fintech isn't the data plumbing any more; it's the product you build on top of clean data. Go build something worth using. Disclosure: I'm the developer behind open-banking.io, which is why I can point you at the cheapest, cer

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.