DEV Community

Reading EU Bank Transactions With Python and PSD2 - Without Buying an eIDAS Certificate

The short version

Instead of obtaining your own eIDAS certificate and registering as a Third Party Provider with every bank, you use an Account Information Service Provider (AISP) that already has the certificate. Your app talks to their API; they talk to the banks. You never touch a certificate.

The trade-off: you depend on an intermediary. But for most indie projects, small businesses, and personal-finance tools, that's a perfectly reasonable trade. You get bank data via a clean REST API instead of fighting with mutual TLS and per-bank registration.

What you'll need

  • Python 3.8+ (stdlib only, no pip installs needed for the basics)
  • An API key from an AISP provider
  • A bank account in a supported EU country

I'm the maintainer of open-banking.io, so I'll use our API as the example - but the pattern is the same for any PSD2 AISP (Enable Banking, Salt Edge, Tink, etc.). Pick whichever fits your coverage and budget.

Step 1: Get your API key

Sign up, grab your API key. Store it as an environment variable:

export OBI_API_KEY="***"

Never hardcode it. If you commit it to git, rotate immediately.

Step 2: Initiate a bank connection

Your user needs to consent to sharing their data. This redirects them to their bank's login page:

import os import json import urllib.request
API_BASE = " https://api.open-banking.io/v1 "
API_KEY = os . env ... EY " ]

def create_session(bank_id: str, return_url: str) -> dict:
    """Start a bank connection. Returns a consent URL for the user."""
    payload = json.dumps({
        "bank_id": bank_id,
        "return_url": return_url,
    }).encode()
    req = urllib.request.Request(
        f"{API_BASE}/sessions",
        data=payload,
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())

# Example: connect to a Danish bank
result = create_session("nordea-dk", "https://yourapp.com/callback")
print(f"Send the user to: {result['consent_url']}")

The user visits consent_url, logs into their bank, and approves. They're redirected back to your return_url with a session token.

Step 3: Fetch accounts and balances

Once the session is authorized:

def get_accounts(session_id: str) -> list:
    req = urllib.request.Request(
        f"{API_BASE}/sessions/{session_id}/accounts",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())["accounts"]

def get_balance(session_id: str, account_id: str) -> dict:
    req = urllib.request.Request(
        f"{API_BASE}/sessions/{session_id}/accounts/{account_id}/balance",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())

accounts = get_accounts(result["session_id"])
for acct in accounts:
    bal = get_balance(result["session_id"], acct["id"])
    print(f"{acct['name']}: {bal['amount']} {bal['currency']}")

Step 4: Fetch transactions

def get_transactions(session_id: str, account_id: str) -> list:
    req = urllib.request.Request(
        f"{API_BASE}/sessions/{session_id}/accounts/{account_id}/transactions",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())["transactions"]

for txn in get_transactions(result["session_id"], accounts[0]["id"]):
    print(f"{txn['date']} | {txn['amount']:>8} | {txn['description'][:50]}")

That's it. You're reading live bank data without ever touching a certificate.

Gotchas worth knowing

  • Consent expiry. PSD2 consents expire (typically 90 days, varies by bank). You'll need to re-authorize periodically. Build that into your app's flow - don't assume a session lasts forever.
  • Booked vs pending. Transactions come in two flavors. booked transactions are finalized; pending are provisional and may change or disappear. If you're building a budgeting tool, track booked as your source of truth and dedupe carefully - the same transaction can appear in both lists during the settlement window.
  • Bank coverage gaps. No AISP covers every EU bank. Before committing, check the provider's coverage list for the banks your users actually need. Smaller regional banks are often the gap.
  • Rate limits. PSD2 APIs enforce per-consent access frequency limits (typically 4x/day for transactions). Cache aggressively. Don't poll in a loop.
  • Privacy model. Some providers store your transaction data server-side. Others (like ours) use client-held encryption keys so the provider literally can't read user data. If you're building for GDPR-conscious users, check which model you're getting.

When you should get the certificate

If you're building a regulated financial product at scale - processing payments (PIS), doing credit checks, or serving thousands of users across many banks - get your own eIDAS certificate and go direct. The intermediary adds latency, cost per call, and a dependency you don't want at scale.

But for personal projects, small SaaS tools, internal dashboards, and anything where you just need to read account data without the enterprise overhead: the AISP path is the pragmatic choice.


Disclosure: I'm John, the maintainer of open-banking.io - a certificate-free PSD2 account information service for EU banks. The code above works with our API, but the pattern applies to any compliant AISP. Questions? Find me in the comments.

Comments

No comments yet. Start the discussion.