April 6, 2026 ~9 min read

Open Banking API Integration — A Practical Guide for Payment Engineers

Everything I wish someone had told me before I started integrating with bank APIs across Europe. Consent flows, certificate nightmares, and why every bank interprets the spec differently.

What Open Banking Actually Means for Engineers

Strip away the marketing and regulatory jargon, and open banking boils down to this: banks are required to expose APIs that let authorized third parties access customer account data and initiate payments — with the customer's explicit consent. That's it. The rest is implementation details, and those details will consume months of your life.

In Europe, this comes from PSD2 (Payment Services Directive 2). In the UK, the CMA's Open Banking Initiative pushed it further with a standardized API spec. Other regions — Australia, Brazil, Saudi Arabia — have their own flavors. But the core concept is the same everywhere: regulated API access to bank accounts.

As a payment engineer, you'll deal with three main capabilities:

47+ Countries with open banking frameworks
6,000+ Banks exposing PSD2 APIs in Europe
90 days Max consent validity before re-auth

The Consent Flow — How It Actually Works

If you've implemented OAuth2 before, the consent flow will feel familiar — because it is OAuth2, with some regulatory constraints bolted on. The bank acts as the authorization server, your application is the client, and the customer grants scoped access to their account data.

Here's what the flow looks like in practice:

User Your App (TPP) Bank API Consent Granted Account Data Initiates Create consent Auth URL User authorizes 1 2 3 4 Request Consent Authorize Fetch

The technical steps break down like this:

  1. Your app (the TPP) calls the bank's API to create a consent resource, specifying what data you need access to.
  2. The bank returns an authorization URL. You redirect the user there.
  3. The user logs into their bank, reviews the permissions, and approves. The bank redirects back to your app with an authorization code.
  4. You exchange that code for an access token and start pulling data.

Here's what the consent creation request looks like against a Berlin Group NextGenPSD2 API:

POST /v1/consents HTTP/1.1
Host: api.examplebank.com
Content-Type: application/json
X-Request-ID: 99391c7e-ad88-49ec-a2ad-99ddcb1f7721
TPP-Redirect-URI: https://yourapp.com/callback

{
  "access": {
    "accounts": [
      { "iban": "DE89370400440532013000" }
    ],
    "balances": [
      { "iban": "DE89370400440532013000" }
    ],
    "transactions": [
      { "iban": "DE89370400440532013000" }
    ]
  },
  "recurringIndicator": true,
  "validUntil": "2026-07-06",
  "frequencyPerDay": 4,
  "combinedServiceIndicator": false
}

Gotcha: That frequencyPerDay field? It's not just a suggestion. Banks enforce it. I've had production integrations start returning 429s because we were polling account balances every 15 minutes instead of the 4-times-per-day limit we declared in the consent. Some banks count the consent creation call itself against the limit. Read the fine print in each bank's developer docs.

Working with TPP APIs — The Reality

In theory, PSD2 and the Berlin Group's NextGenPSD2 spec standardize everything. In practice, every bank interprets the spec differently. I've integrated with about 30 banks across Europe, and I can tell you: no two implementations are identical.

Some examples of what you'll encounter:

Certificate Management — The Real Pain Point

Before you make a single API call, you need certificates. PSD2 requires eIDAS certificates issued by a Qualified Trust Service Provider (QTSP). You'll need two types:

Getting these certificates takes 2-4 weeks from a QTSP like DigiCert or Entrust. They cost real money. And they expire — usually after 1-2 years. Here's a typical mTLS setup:

import httpx

client = httpx.Client(
    cert=("/path/to/qwac.pem", "/path/to/qwac-key.pem"),
    verify="/path/to/bank-ca-bundle.pem",
    headers={
        "X-Request-ID": str(uuid4()),
        "TPP-Signature-Certificate": load_qseal_base64(),
    },
    timeout=30.0,
)

# Fetch accounts after consent is authorized
response = client.get(
    "https://api.examplebank.com/v1/accounts",
    headers={
        "Consent-ID": "psd2-consent-1234",
        "Authorization": f"Bearer {access_token}",
    },
)
accounts = response.json()

Warning: Never store eIDAS certificates in your repo or container images. Use a secrets manager and mount them at runtime. Certificate revocation in the eIDAS ecosystem is painful — if your QWAC gets compromised, you're looking at days of downtime while you get a replacement issued. I've seen it happen. Set up monitoring for certificate expiry at least 60 days out.

Building a Reliable Aggregation Layer

Once you've integrated with more than a handful of banks, you'll realize you need an abstraction layer. Each bank has its own quirks, error codes, rate limits, and data formats. Without a normalization layer, your application code becomes a mess of bank-specific conditionals.

Here's the architecture I've landed on after a few iterations:

  1. Bank Adapter Layer — one adapter per bank (or bank API spec). Each adapter handles authentication, request signing, and response parsing for that specific bank. This is where all the ugly bank-specific logic lives.
  2. Normalization Layer — transforms bank-specific responses into your internal canonical format. Dates, amounts, currency codes, transaction categories — all standardized here.
  3. Consent Manager — tracks consent status, handles token refresh, manages re-authentication flows when consents expire (remember the 90-day limit).
  4. Retry & Circuit Breaker — bank APIs go down. A lot. You need per-bank circuit breakers so one flaky bank doesn't take down your entire aggregation service.
# Simplified bank adapter pattern
class BankAdapter:
    def get_accounts(self, consent_id: str) -> list[Account]:
        raise NotImplementedError

class DeutscheBankAdapter(BankAdapter):
    BASE_URL = "https://simulator-api.db.com/gw/oidc/psd2"

    def get_accounts(self, consent_id: str) -> list[Account]:
        resp = self.client.get(
            f"{self.BASE_URL}/v1/accounts",
            headers={"Consent-ID": consent_id},
        )
        return [
            Account(
                id=a["resourceId"],
                iban=a.get("iban"),
                name=a.get("name", ""),
                currency=a["currency"],
                balance=self._parse_balance(a),
            )
            for a in resp.json()["accounts"]
        ]

    def _parse_balance(self, account_data: dict) -> Decimal:
        # Deutsche Bank nests balances differently than the spec suggests
        balances = account_data.get("_links", {}).get("balances", {})
        # ... bank-specific parsing logic

Tip: Build your adapter tests against recorded bank API responses, not mocks you wrote yourself. Record real sandbox responses and replay them. When a bank changes their API behavior (and they will, often without notice), your tests will catch it immediately.

Open Banking vs Screen Scraping

Before PSD2, the only way to access bank account data was screen scraping — logging in as the user with their credentials and parsing the HTML. Some aggregators still do this for banks that haven't fully implemented open banking APIs. Here's why that matters:

Aspect Open Banking APIs Screen Scraping
User credentials Never shared with TPP TPP stores user's bank login
Consent granularity Scoped to specific accounts/data Full account access
Reliability Structured API responses Breaks when UI changes
Regulatory status Fully regulated under PSD2 Grey area / being phased out
SCA compliance Built into the flow Bypasses SCA requirements
Rate limits Defined per consent Risk of IP blocking
Data freshness Near real-time (within limits) Depends on scraping frequency
Setup complexity High (certificates, registration) Low (but fragile)

The bottom line: screen scraping is a liability. It's fragile, it's a security risk (you're storing user credentials), and regulators are actively shutting it down. If you're building anything new, go API-first. The upfront investment in eIDAS certificates and bank integrations pays off in reliability and compliance.

Practical Gotchas I've Learned the Hard Way

A few things that aren't in any spec document but will save you weeks of debugging:

Pro tip: Join the Berlin Group's NextGenPSD2 implementation support channels and your national open banking community (like Open Banking UK's Slack). When a bank's API starts behaving strangely, chances are another TPP has already figured out the workaround.

Wrapping Up

Open banking integration is one of those things that looks straightforward on paper and turns into a multi-month project once you start dealing with real banks. The specs are a good starting point, but the real work is in handling the inconsistencies, managing certificates, and building an abstraction layer robust enough to survive bank API changes.

My advice: start with one or two banks in a single market, get your adapter pattern right, then expand. Don't try to integrate 20 banks at once. And budget at least 30% of your integration timeline for "things the spec didn't mention." You'll need it.

References

Disclaimer: This article reflects the author's personal experience and opinions. Product names, logos, and brands are property of their respective owners. Open banking regulations vary by jurisdiction — always consult with your compliance and legal teams before implementing integrations that handle customer financial data. Code examples are simplified for illustration and should not be used in production without proper security review.