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:
- Account Information Services (AIS) — read account balances, transaction history, account details. This is what powers aggregation apps like Plaid-connected dashboards.
- Payment Initiation Services (PIS) — trigger payments directly from a customer's bank account. Think bank-to-bank transfers without card rails.
- Confirmation of Funds (CoF) — check if an account has sufficient funds for a specific amount. Less common, but useful for certain checkout flows.
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:
The technical steps break down like this:
- Your app (the TPP) calls the bank's API to create a consent resource, specifying what data you need access to.
- The bank returns an authorization URL. You redirect the user there.
- The user logs into their bank, reviews the permissions, and approves. The bank redirects back to your app with an authorization code.
- 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:
- Bank A returns transaction dates as
2026-04-06, Bank B returns06/04/2026, Bank C returns a Unix timestamp. - Some banks require the
PSU-IP-Addressheader on every call. Others reject requests that include it on background (non-PSU-present) calls. - The
bookingStatusparameter acceptsbooked,pending, orboth— except at banks that only supportbookedand return a 400 if you ask for anything else. - Pagination: some banks use
nextlinks, some use offset parameters, some just dump everything in one response regardless of size.
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:
- QWAC (Qualified Website Authentication Certificate) — used for TLS mutual authentication. This is your mTLS client certificate.
- QSeal (Qualified Electronic Seal) — used to digitally sign your API requests. Not all banks require it, but many do.
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:
- 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.
- Normalization Layer — transforms bank-specific responses into your internal canonical format. Dates, amounts, currency codes, transaction categories — all standardized here.
- Consent Manager — tracks consent status, handles token refresh, manages re-authentication flows when consents expire (remember the 90-day limit).
- 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:
- Sandbox vs production divergence. Bank sandboxes are often months behind their production APIs. I've had integrations pass every sandbox test and fail immediately in production because the bank updated their prod API without touching the sandbox.
- The 90-day re-authentication wall. PSD2 requires Strong Customer Authentication (SCA) every 90 days for AIS consents. Your users will need to re-authenticate with their bank quarterly. Build this into your UX — send reminders before consent expires, and handle the re-auth flow gracefully.
- Rate limits are per-consent, not per-app. If you have 10,000 users connected to the same bank, each consent has its own rate limit. But some banks also enforce global TPP-level rate limits. You need both per-consent and global throttling.
- Error responses are not standardized. The spec defines error codes, but banks return wildly different error payloads. Build a robust error normalization layer and log raw responses for debugging.
- Weekend maintenance windows. Many European banks take their APIs offline for maintenance on weekends. Schedule your batch data pulls accordingly.
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
- PSD2 — Directive (EU) 2015/2366 (EUR-Lex)
- Berlin Group — NextGenPSD2 Access to Account Framework
- UK Open Banking — API Standards
- EBA — RTS on Strong Customer Authentication
- RFC 6749 — The OAuth 2.0 Authorization Framework
- ESMA — Open Finance and Data Sharing
- Open Banking Tracker — Global Adoption Map
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.