BLOG POST
PSD2 and Open Banking Integration: A Developer's Field Guide

The Regulatory Architecture
Before we touch any code, you need the mental model. I'll keep this short because the regulatory docs are 300+ pages and most of it doesn't matter for your day-to-day engineering work. But some of it absolutely does, and if you skip it you'll build the wrong thing.
PSD2 (Directive 2015/2366) mandates two things that matter to us:
Strong Customer Authentication (SCA) - every electronic payment must be authenticated with at least two independent factors from three categories: knowledge (password, PIN), possession (phone, hardware token), and inherence (biometrics). For remote payments, there's a "dynamic linking" requirement: the authentication code must be tied to a specific amount and payee. Change either, and the code is invalid. This isn't optional. This is why your users get redirected to their banking app mid-transaction.
SCA exemptions exist for low-value transactions (under €30), contactless under €50, trusted beneficiaries, recurring payments after initial auth, and transactions passing a Transaction Risk Analysis threshold tied to the provider's fraud rates. If you're building a PIS (payment initiation) flow, you need to understand these exemptions because they directly affect your conversion rates.
XS2A (Access to Account) - banks must provide licensed third parties with API access to customer payment accounts. Three flavors:

That's it. Everything else - the four competing standards, the eIDAS certificates, the FAPI profiles, the consent management - is implementation detail. Important implementation detail that will consume months of your life, but implementation detail nonetheless.
Enforcement Timeline (The Real One)
The official timeline and the actual timeline are two different things. Here's what actually happened:

The takeaway: if you're building today, SCA is real and enforcement is imminent. The sandbox-optional era is over. Your APIs need to work with mTLS and eIDAS certificates against production bank endpoints. No more excuses.
Entity Taxonomy: Who Does What
You'll see these acronyms constantly. Know them:

ASPSPs are the banks. They build and maintain the APIs. They do this with varying degrees of enthusiasm (mostly zero).
AISPs need NCA registration and face lighter requirements - no capital requirement. This is the path if you're building a personal finance app or an account aggregation service.
PISPs need full Payment Institution authorization with minimum €50,000 capital. This is the path if you're initiating payments.
TPP is the umbrella term for both. Each TPP identifies itself to banks using eIDAS certificates - qualified digital certificates with the TPP's authorization number, NCA identifier, and permitted roles encoded as ASN.1 OIDs. More on these certificates later. You will become very familiar with them.
UK Open Banking: The Parallel Universe That Actually Works
While PSD2 created the legal right to account access, it deliberately avoided specifying how. The UK took a completely different approach and it's worth understanding because the contrast is instructive.
In 2016, the Competition and Markets Authority concluded that the nine largest UK banks weren't competing hard enough. The CMA Order of February 2017 forced them - the CMA9 (Barclays, HSBC, Lloyds, Nationwide, NatWest/RBS, Santander UK, AIB, Bank of Ireland, Danske Bank UK) - to fund an implementation entity and build standardized APIs. Together they held over 90% of UK current accounts.
The critical difference: the UK mandated a single API specification with detailed security profiles, customer experience guidelines, and a centralized trust framework. Continental Europe got "technology neutral" regulation and "market-led" standards. Guess which approach produced better APIs.
Open Banking Limited (the OBIE) designed the API specs, operated the Open Banking Directory, managed conformance testing, and monitored CMA9 compliance. The FCA authorized firms, OBIE governed the tech standards, the CMA oversaw the competition remedy. This tripartite structure was unique to the UK.
The Open Banking Directory functioned as a trust framework and identity infrastructure. It managed org identities, issued certificates, generated Software Statement Assertions (SSAs) - signed JWTs containing a TPP's regulatory status and authorized roles - and performed real-time checks against the FCA register. Initially, OBIE issued its own certificates (OBWAC and OBSeal) as a pragmatic bridge while the eIDAS QTSP market matured. After September 2019, eIDAS became legally required, but the FCA allowed an adjustment period.
UK Open Banking went live in January 2018 - 20 months before the continental deadline. By end of 2020, the UK has 294 regulated providers, 102 with live products, and over 2.5 million users. This is what happens when you mandate a single standard instead of leaving it to "the market."
Four API Standards, One Continent, Infinite Pain
This is where it gets ugly. The absence of a mandated EU-wide API specification produced four major competing standards. Each with different endpoint structures, authentication patterns, signing mechanisms, and data models. If you want pan-European coverage, you deal with all of them.
Let me walk through each one from a developer's perspective.
Berlin Group NextGenPSD2
This is the big one. Adopted by over 3,600 banks (75%+ of European banks) across Germany, Austria, the Nordics, CEE, Italy, and Iberia. If you're connecting to any European bank outside the UK and France, this is probably what you're looking at.
The framework ships as operational rules, implementation guidelines, and OpenAPI 3.0 spec files. The REST structure is clean:

Payment products include sepa-credit-transfers, instant-sepa-credit-transfers, cross-border-credit-transfers, and country-specific domestic products. Version 1.3.6 (February 2020) is what most banks are running in production right now.
Berlin Group defines four SCA approaches - redirect, OAuth2 redirect, decoupled, and embedded - giving banks wide latitude. This sounds flexible. In practice it means every bank picks a different one and you implement all four.
A consent request looks like this:
{
"access": {
"accounts": [{"iban": "DE89370400440532013000"}],
"balances": [{"iban": "DE89370400440532013000"}],
"transactions": [{"iban": "DE89370400440532013000"}]
},
"recurringIndicator": true,
"validUntil": "2021-12-31",
"frequencyPerDay": 4
}
That frequencyPerDay: 4 isn't arbitrary - the RTS caps background data refresh at 4 times per 24 hours without active user involvement. You need to build rate limiting into your polling logic.
STET (France)
Used by BNP Paribas, Société Générale, Crédit Agricole, La Banque Postale, and the other French heavyweights. STET is architecturally the most interesting standard because it actually uses HATEOAS-driven navigation - responses contain hypermedia links guiding you through the flow. REST purists would weep with joy. The rest of us have to parse and follow dynamically-generated URLs.
Key differences from Berlin Group:

The ISO 20022 status codes are actually better than what Berlin Group uses, but they're unfamiliar to most developers and require mapping to your internal status model.
PolishAPI
This is the odd one. Developed by the Polish Bank Association, it uses a POST-only API design. Every operation - including reads - uses HTTP POST. The justification: you can digitally sign every request body (good for GDPR audit trails) and you avoid URL-length constraints.
The practical consequence: standard OAuth2 client libraries don't work out of the box. PolishAPI adds mandatory custom OAuth2 extensions including a scope_details parameter. If you're using any standard OAuth2 library (and you should be), you'll need to patch it or wrap it.

If you're only targeting the Polish market, you deal with it. If you're building pan-European, PolishAPI is one of those things that makes you reconsider whether aggregators are worth the per-transaction fee. (They are.)
Open Banking UK
The richest data model and most prescriptive specification of the lot. Endpoints follow /{resource-group}/{resource}:

Every response is wrapped in a standard envelope:
{
"Data": { /* actual payload */ },
"Risk": {},
"Links": {
"Self": "https://api.bank.co.uk/aisp/accounts"
},
"Meta": {
"TotalPages": 1,
"FirstAvailableDateTime": "2020-01-01T00:00:00+00:00",
"LastAvailableDateTime": "2020-12-31T23:59:59+00:00"
}
}The Data/Risk/Links/Meta wrapper is consistent across all endpoints. If you're writing a Go client, define a generic response struct and deserialize into it:
type OBResponse[T any] struct {
Data T `json:"Data"`
Risk Risk `json:"Risk"`
Links Links `json:"Links"`
Meta Meta `json:"Meta"`
}Standard Comparison Matrix
| Berlin Group | STET | PolishAPI | OB UK | |
|---|---|---|---|---|
| Coverage | ~3,600 banks, 20+ countries | French banks, ~20 | Polish banks, ~15 | UK CMA9 + voluntary |
| Spec format | OpenAPI 3.0 | OpenAPI 3.0 | Swagger 2.0 | OpenAPI 3.0 |
| HTTP methods | Standard REST | REST + HATEOAS | POST-only | Standard REST |
| SCA approaches | 4 (redirect, OAuth2, decoupled, embedded) | Redirect, decoupled | Redirect | Redirect only (FAPI hybrid) |
| Message signing | HTTP Signature (Digest + Signature hdr) | HTTP Signature | JWS | JWS detached (x-jws-signature header) |
| Auth framework | OAuth 2.0 | OAuth 2.0 | OAuth 2.0 (custom ext.) | FAPI 1.0 Advanced (R/W) |
| Payment status | Custom codes | ISO 20022 | Custom codes | Custom codes |
| Refresh tokens | Bank discretion | Up to 180 days | Bank discretion | Bank discretion |
| Data richness | Minimal | Moderate | Minimal | Rich (14+ AIS resource types) |
| Integration time | 2–14 days/bank (varies wildly) | ~1 week/bank | ~2 weeks/bank (custom OAuth) | 2–5 days/bank (standardized) |
A developer connecting to a German bank (redirect SCA, SEPA credit transfers, HTTP Signature headers) faces a completely different integration than a UK bank (FAPI hybrid flow with response_type=code id_token, JWS detached signatures, OBIE Directory enrollment) or a French bank (HATEOAS navigation, ISO 20022 status codes) or a Polish bank (POST-only, custom OAuth extensions).
This is the reality of "technology-neutral regulation." Four standards. Thousands of bank-specific interpretations within each standard. Welcome to European fintech.
The Security Stack: eIDAS, mTLS, FAPI, and OAuth
Now we get to the part that will consume most of your infrastructure work. PSD2's security architecture layers multiple standards into a defense-in-depth model. If you've only done standard OAuth 2.0 integrations, prepare yourself - this is several levels above that.
eIDAS Certificates
PSD2 RTS Article 34 mandates qualified certificates per ETSI TS 119 495 for TPP identification. You need two types:

Both certificates contain PSD2-specific fields that banks parse to verify your authorization:

Banks extract these fields and validate them against the EBA register. If your certificate says PSP_AI but you try to call a PIS endpoint, you'll get rejected.
Getting certificates. You need a Qualified Trust Service Provider (QTSP). In 2019 the market was tiny. It's better now. Major providers:
| QTSP | Approx. Cost/yr | Notes |
|---|---|---|
| QuoVadis/DigiCert | €1,000–€2,000 | Market leader, good docs |
| Sectigo | €500–€1,500 | USB token delivery |
| Multicert | €800–€1,500 | Portuguese QTSP |
| Microsec | €500–€1,200 | Hungarian, good support |
The process: generate RSA 2048–4096 key pair, create a CSR with PSD2-specific OIDs, submit to QTSP, they validate against your NCA register entry, you get the cert in 1–5 business days.
For sandbox testing, you can generate self-signed eIDAS-format certificates with OpenSSL. Here's the config file I use:
# eidas-sandbox.cnf
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
organizationName = Your Company Name
organizationIdentifier = PSDGB-FCA-123456
countryName = GB
[v3_req]
keyUsage = digitalSignature
extendedKeyUsage = clientAuth
subjectAltName = @alt_names
# PSD2 QCStatements
1.3.6.1.5.5.7.1.3 = ASN1:SEQUENCE:qcstatements
[qcstatements]
psd2 = SEQUENCE:psd2_statement
[psd2_statement]
statementId = OID:0.4.0.19495.2
statementInfo = SEQUENCE:psd2_info
[psd2_info]
rolesOfPSP = SEQUENCE:psd2_roles
nCAName = UTF8:Financial Conduct Authority
nCAId = UTF8:PSDGB-FCA
[psd2_roles]
role1 = SEQUENCE:role_psp_ai
[role_psp_ai]
roleOfPSP = OID:0.4.0.19495.1.3
roleOfPSPName = UTF8:PSP_AI
[alt_names]
DNS.1 = sandbox.your-tpp.comGenerate the cert:
openssl req -new -newkey rsa:2048 -nodes \
-keyout qwac-sandbox.key \
-out qwac-sandbox.csr \
-config eidas-sandbox.cnf
openssl x509 -req -days 365 \
-in qwac-sandbox.csr \
-signkey qwac-sandbox.key \
-out qwac-sandbox.crt \
-extensions v3_req \
-extfile eidas-sandbox.cnf
Most bank sandboxes accept self-signed certs. Some don't. You'll find out the hard way.
Mutual TLS
mTLS uses the QWAC at the transport layer. During the TLS handshake, the bank sends a CertificateRequest, you respond with your QWAC and prove private key possession via CertificateVerify. The bank extracts your identity from the certificate's organizationIdentifier.
The important secondary function: certificate-bound access tokens per RFC 8705. When the bank's authorization server issues you an access token over an mTLS connection, it computes the SHA-256 hash of your TLS certificate and binds it to the token:

For JWT access tokens, the binding appears as:
{
"cnf": {
"x5t#S256": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo"
}
}This prevents stolen token replay - even if an attacker intercepts your access token, they can't use it without your private key. This is table stakes for PSD2.
In Go, configuring mTLS looks like:
cert, err := tls.LoadX509KeyPair("qwac.crt", "qwac.key")
if err != nil {
log.Fatalf("failed to load eIDAS certificate: %v", err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}In Java with Quarkus or any JAX-RS framework, configure the SSLContext with a KeyManager loaded from your PKCS12 keystore containing the QWAC cert and private key. I won't dump the whole factory pattern here but you know the drill - KeyManagerFactory.getInstance("SunX509"), load the keystore, init the SSL context.
FAPI Security Profiles
Open Banking UK adopted FAPI 1.0 Advanced (Read-Write), the strictest OpenID Foundation security profile. If you're integrating with UK banks, you need to understand this. If you're integrating with Berlin Group banks, you can skip ahead - most of them use vanilla OAuth 2.0 + mTLS.
FAPI Advanced requirements that will affect your code:

The hybrid flow (response_type=code id_token) is the key difference from standard OAuth2. The ID Token returned alongside the authorization code contains hashes (c_hash of the code, at_hash of the access token, s_hash of the state) that let you verify response integrity client-side before hitting the token endpoint. This is a defense against authorization response manipulation.
The Consent Flow In Practice
Let me walk through the full sequence for both Berlin Group and UK Open Banking, because they're materially different:
Berlin Group AIS Consent Flow:


Notice the extra steps in the UK flow: client credentials grant first, consent resource creation, signed JWT request object for the redirect. It's more work but it's also more secure and more standardized.
Token lifecycles are aggressively short. Access tokens typically live 600 seconds (10 minutes) in FAPI implementations. Build your token management around frequent refresh. And AIS consents expire after a maximum 90 days - after which the user must re-authenticate entirely. Build re-auth flows as a first-class UX concern, not an afterthought.
Integration Walkthrough: From Zero to First API Call
Here's the actual sequence you'll follow. I'll be specific about what costs time.
Step 1: NCA Authorization
Submit an application to your National Competent Authority. AISP registration is lighter (no capital requirement). PISP authorization requires minimum €50,000 capital plus a lot more paperwork - business model documentation, data privacy controls, governance structures, financial stability evidence, integrity checks on directors.
Timeline: weeks to months depending on the NCA. The FCA is relatively fast. BaFin is... not. Factor this into project timelines. Your engineering work is blocked until authorization is granted because you need the authorization number for your eIDAS certificates.
Step 2: Obtain eIDAS Certificates
Generate RSA key pairs, create CSRs with PSD2 OIDs, submit to a QTSP, wait for validation.
Your organizationIdentifier encodes your authorization: PSDGB-FCA-123456 for a UK FCA-authorized firm, PSDDE-BAFIN-654321 for a German BaFin-authorized firm, etc.
Store private keys in an HSM. Not in a file on disk. Not in a Kubernetes secret. In an actual hardware security module. More on this in the hosting section.
Step 3: Directory Enrollment (UK)
UK-specific. Register with the Open Banking Directory, create Software Statements, upload certificates, generate Software Statement Assertions (SSAs). The Directory provides JWKS endpoints, OCSP for revocation checking, and APIs for discovering enrolled ASPSPs.
Step 4: Dynamic Client Registration
For each bank you want to connect to, you perform DCR:
- Query their OIDC discovery endpoint:
GET /.well-known/openid-configuration - Construct and sign a registration JWT containing the SSA (UK) or eIDAS cert (EU)
- POST it to the bank's registration endpoint over mTLS
# Example DCR call
curl -X POST https://api.bank.com/register \
--key qwac.key \
--cert qwac.crt \
-H 'Content-Type: application/jose' \
-d 'eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InF3YWMta2V5LTEifQ...'
The response gives you OAuth client credentials (client_id, maybe client_secret), confirmed redirect URIs, and authorized scopes. You repeat this for every bank. If you're connecting to 50 banks, that's 50 DCR registrations, each potentially failing in different ways.
Step 5: Consent and Data Access
Now you can actually make API calls. Here's a full Berlin Group AIS integration in Python:
import requests
import uuid
# Configure mTLS session
session = requests.Session()
session.cert = ('qwac.crt', 'qwac.key')
session.verify = True # verify bank's server cert
BASE_URL = 'https://xs2a.bank.com'
# 1. Create consent
consent_response = session.post(
f'{BASE_URL}/v1/consents',
headers={
'Content-Type': 'application/json',
'X-Request-ID': str(uuid.uuid4()),
'PSU-IP-Address': '192.168.8.78',
'TPP-Redirect-URI': 'https://your-tpp.com/callback',
'TPP-Nok-Redirect-URI': 'https://your-tpp.com/callback/error',
},
json={
'access': {
'accounts': [{'iban': 'DE89370400440532013000'}],
'balances': [{'iban': 'DE89370400440532013000'}],
'transactions': [{'iban': 'DE89370400440532013000'}],
},
'recurringIndicator': True,
'validUntil': '2021-06-30',
'frequencyPerDay': 4,
}
)
consent = consent_response.json()
consent_id = consent['consentId']
sca_redirect = consent['_links']['scaRedirect']['href']
print(f'Redirect user to: {sca_redirect}')
# User authenticates at bank, gets redirected back with auth code
# 2. Exchange auth code for tokens
token_response = session.post(
f'{BASE_URL}/oauth/token',
data={
'grant_type': 'authorization_code',
'code': 'AUTH_CODE_FROM_CALLBACK',
'redirect_uri': 'https://your-tpp.com/callback',
}
)
tokens = token_response.json()
# 3. Fetch accounts
accounts_response = session.get(
f'{BASE_URL}/v1/accounts',
headers={
'Authorization': f'Bearer {tokens["access_token"]}',
'X-Request-ID': str(uuid.uuid4()),
'Consent-ID': consent_id,
}
)
accounts = accounts_response.json()
# 4. Fetch transactions for first account
account_id = accounts['accounts'][0]['resourceId']
txn_response = session.get(
f'{BASE_URL}/v1/accounts/{account_id}/transactions',
headers={
'Authorization': f'Bearer {tokens["access_token"]}',
'X-Request-ID': str(uuid.uuid4()),
'Consent-ID': consent_id,
},
params={
'dateFrom': '2020-01-01',
'dateTo': '2020-12-31',
'bookingStatus': 'booked',
}
)
transactions = txn_response.json()And the equivalent in Go for those of us who prefer it:
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/google/uuid"
)
type ConsentRequest struct {
Access Access `json:"access"`
RecurringIndicator bool `json:"recurringIndicator"`
ValidUntil string `json:"validUntil"`
FrequencyPerDay int `json:"frequencyPerDay"`
}
type Access struct {
Accounts []AccountRef `json:"accounts"`
Balances []AccountRef `json:"balances"`
Transactions []AccountRef `json:"transactions"`
}
type AccountRef struct {
IBAN string `json:"iban"`
}
func newMTLSClient(certFile, keyFile string) (*http.Client, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("load eIDAS cert: %w", err)
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
},
},
}, nil
}
func createConsent(client *http.Client, baseURL, iban string) (string, string, error) {
req := ConsentRequest{
Access: Access{
Accounts: []AccountRef{{IBAN: iban}},
Balances: []AccountRef{{IBAN: iban}},
Transactions: []AccountRef{{IBAN: iban}},
},
RecurringIndicator: true,
ValidUntil: "2021-06-30",
FrequencyPerDay: 4,
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", baseURL+"/v1/consents", bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-Request-ID", uuid.New().String())
httpReq.Header.Set("PSU-IP-Address", "192.168.8.78")
httpReq.Header.Set("TPP-Redirect-URI", "https://your-tpp.com/callback")
resp, err := client.Do(httpReq)
if err != nil {
return "", "", fmt.Errorf("consent request: %w", err)
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
consentID := result["consentId"].(string)
links := result["_links"].(map[string]interface{})
scaRedirect := links["scaRedirect"].(map[string]interface{})["href"].(string)
return consentID, scaRedirect, nil
}
Berlin Group also requires HTTP Signature headers for request signing with your QSeal certificate. You'll need Digest (SHA-256 of request body), Signature (computed using QSeal), and TPP-Signature-Certificate (base64 QSeal cert). Not every bank enforces this in practice, but it's in the spec and the compliant ones will reject you without it.
Hosting and Deployment: Infrastructure for Regulated Fintech
Now the part most PSD2 guides skip entirely: actually running this thing in production. You're dealing with regulated financial infrastructure. The bar is higher than your average SaaS deployment.
Reference Architecture

I run this on Kubernetes (EKS in my case, but GKE or self-managed works fine). The key architectural decisions:
Separate the bank adapter layer. Each API standard (Berlin Group, STET, OB UK, PolishAPI) gets its own adapter service that translates between your internal canonical model and the bank-specific API. This is where most of the per-bank customization lives. I use a Go service for this because the HTTP client configuration (mTLS, custom headers, cert-bound tokens) is much cleaner in Go than in Java's HttpClient.
Consent management is its own service. Consent states (AwaitingAuthorisation, Authorised, Rejected, Revoked, Expired), expiry tracking, 90-day re-auth scheduling, and rate limiting (4 requests/day cap) - this gets complex enough to warrant isolation. PostgreSQL backend with advisory locks for concurrent consent state transitions.
Async everything from the bank adapter outward. Bank APIs are unreliable. Timeouts, 5xx errors, rate limits - you don't want synchronous request chains. I use NATS JetStream (or Kafka if you prefer) for bank API events, retry queues, and webhook delivery. Bank adapter → NATS → consumer services → PostgreSQL.
Audit logging is not optional. PSD2 requires you to demonstrate what data you accessed, when, with what consent, for how long. Every bank API call gets logged to an append-only audit table: timestamp, consent ID, bank, endpoint, response code, data hash (not the data itself). PostgreSQL with SERIALIZABLE isolation on the audit table.
Private Key Management
Do not store eIDAS private keys in Kubernetes secrets. Do not store them in environment variables. Do not store them in a file on disk with chmod 600 and call it a day. Auditors will find it. Regulators will not be amused.
Hardware Security Modules are the gold standard:
| Category | Option | Cost |
|---|---|---|
| On-premise | Entrust nShield HSMs | €10,000+ capital + annual maintenance |
| Thales Luna HSMs | €10,000+ capital + annual maintenance | |
| Cloud-managed | AWS CloudHSM (~$1.60/hr per HSM, need 2 for HA) | ~$2,400/month for HA pair |
| Azure Dedicated HSM | Similar pricing | |
| GCP Cloud HSM | Similar pricing | |
| Budget alternative | AWS KMS with custom key store | Pay per operation |
| QTSP remote signing service | ~€0.02/operation | |
| Sectigo USB token delivery (keys never leave device) | Included with cert |
For the signing operations (QSeal → HTTP Signature / JWS), you can use a PKCS#11 interface to the HSM from your Go/Java services. Performance isn't an issue - you're signing a few hundred requests per second at most, not doing bulk crypto.
If you're a startup and HSM costs are prohibitive, the QTSP remote signing service is a viable bridge. You call their API, they sign with your key held in their HSM, you get the signature back. Latency adds ~50ms per request but for PSD2 API calls that's noise.
API Gateway Selection
Your gateway needs to handle mTLS, OAuth 2.0 token validation, and eIDAS certificate attribute parsing. This rules out lightweight proxies like Nginx (without significant Lua scripting).
| Gateway | PSD2 Suitability |
|---|---|
| Kong | Native mTLS, OAuth 2.0, DCR support. Used by Raiffeisen Bank International. Lua plugin system lets you parse eIDAS cert fields. |
| Tyk | FAPI compliant, mTLS, JWT validation. ISO 27001 and SOC2 certified. Go-based - easy to extend if you know the language. |
| Google Apigee | Dedicated "Open Banking APIx" product. Enterprise-grade but enterprise-priced. |
| WSO2 OB | Open source. Built-in DCR and consent management for both UK OB and Berlin Group. Java-based. Heavy. But feature-complete. |
I use Kong with a custom Lua plugin that extracts organizationIdentifier and QCStatement OIDs from the client certificate and adds them as upstream headers. The adapter services then validate the TPP role against the requested endpoint without re-parsing the certificate.
GDPR Intersection
PSD2 and GDPR create a Venn diagram of overlapping and sometimes conflicting requirements.
PSD2 consent (user authorizes account access) and GDPR consent (legal basis for data processing) are different legal mechanisms. PSD2 access typically relies on GDPR's "contractual necessity" or "legitimate interest" basis, not GDPR consent. You need both, but they're separate.
Data minimization: request only the permissions you actually need. If you only need balances, don't request ReadTransactionsDetail just because you can.
Data retention creates tension: AML rules want 5+ years of records, GDPR deletion rights pull the other direction. I keep financial data in hot storage for the consent duration plus 90 days, then archive to encrypted cold storage with restricted access for the AML retention period.
Cross-border hosting: keep data within the EU/EEA. If you're using AWS, eu-west-1 or eu-central-1. Standard Contractual Clauses if you must use non-EU hosting. Don't make this harder than it needs to be.
Aggregator Platforms: The Middleware That Makes PSD2 Usable
The fragmentation described above created an entire industry of platforms that absorb the complexity and expose a unified API. For most developers, this is the practical path to production. I'll cover each one with technical specifics.
Tink
Swedish. Connected to 3,400+ European banks through a single API covering AIS and PIS. By August 2019, Tink had 110+ engineers working full-time on PSD2 integration, racing to connect 100+ bank APIs covering ~90% of the European population before the September deadline. That effort is instructive - if a company with 110 engineers found it a massive undertaking, imagine doing it with a team of 3.
Tink's differentiator is data enrichment: transaction categorization, spending analysis, income detection, financial insights built on top of raw PSD2 data. Their Tink Link widget provides a pre-built bank selection and authentication UI that you embed in your application. Useful for prototyping and MVPs, but you'll want your own UX for production.
TrueLayer
London-based. Licensed as both AISP and PISP. Connects to 98% of UK banks and 95%+ of European bank accounts entirely through APIs - no screen scraping. They provide SDKs for JavaScript, Python, Java, and .NET, with webhook notifications for payment status rather than polling.
If your primary use case is account-to-account payments for e-commerce checkout, crypto exchanges, or investment platforms, TrueLayer is strong. Pricing is transaction-based, typically 0.3–1.5% depending on volume.
Yapily
Pure infrastructure play. ~2,000 banks, 19 European countries, direct API connections with no intermediary layers. The key differentiator is white-label flexibility - Yapily doesn't impose rigid workflows. You build fully customized user journeys. They also let enterprises operate under Yapily's regulatory license without obtaining their own - useful if your NCA authorization is still in progress.
If I were building a deeply customized single-market integration where UX control matters, Yapily would be my first call.
Token.io
Infrastructure "plumbing" rather than a consumer-facing product. Selected by Mastercard to power its open banking connectivity hub. Their TokenOS platform processes $150 billion+ in annualized transaction volume and was the first PISP to complete testing with all CMA9 banks. Bank clients include HSBC (Open Payments), BNP Paribas (Instanea), and Santander.
Token.io is what you use when you need bank-grade reliability and your counterparties are enterprise banks that need assurance from a Mastercard-backed provider.
Nordigen
The disruptor. Launched a free-tier model for account data. Their logic: PSD2 mandates free API access from banks, so the underlying cost approaches zero. The incumbents charge because they built expensive screen-scraping infrastructure. Nordigen connects only to regulated PSD2 APIs - no screen scraping, no credential storage.
Free tier: account information (balances, transactions, account holder details), 90-day recurring consent, 4x daily background refresh, 2,300+ European banks. Revenue comes from premium features: ML transaction categorization, credit scoring, financial insights, enterprise support.
If you're a startup or indie developer prototyping a European fintech product, Nordigen is the path of least resistance. Sign up, get an API key, connect to a sandbox, and you're querying bank accounts within an hour. The data quality is only as good as the underlying bank APIs (which varies), but for prototyping and small-scale production, it works.
Salt Edge
Uniquely serves both sides: their Open Banking Gateway (Priora) provides PSD2 connections for TPPs, while simultaneously helping banks deploy PSD2-compliant APIs. 3,100+ financial institutions across 61 countries. They use a hybrid approach - PSD2 API connections where available, screen scraping where not.
Salt Edge also publishes some of the best technical analysis of eIDAS certificate challenges. If you're debugging certificate renewal across multiple banks, their blog posts are worth reading.
Direct vs. Aggregator Decision Matrix
| Factor | Use Aggregator | Go Direct |
|---|---|---|
| Market coverage | Pan-European (5+ countries) | Single market, <10 banks |
| Time to market | Weeks | Months |
| Team size | 1–5 devs | 5+ devs with fintech exp |
| eIDAS cert management | Aggregator handles it | Your problem |
| Per-transaction cost | $0.01–$0.50 per call | Near zero after setup |
| UX control | Constrained by platform | Full control |
| Data enrichment | Often included | Build it yourself |
| Bank relationship | Through aggregator | Direct bilateral |
| Premium (non-PSD2) data | Limited | Negotiate bilateral APIs |
| Regulatory licensing | May operate under theirs | Need your own |
My recommendation for most teams: start with an aggregator, go direct for high-volume single-bank integrations. The operational burden of managing eIDAS certificates, handling bank-specific quirks, and keeping up with spec changes across four standards is not something a small team should take on unless the economics of per-transaction fees don't work at your scale.
What Actually Goes Wrong: Field Notes from Production
This section is the one I wish I'd had before I started. These aren't theoretical concerns - they're what I've hit in production, what aggregator engineering teams have documented publicly, and what the PSD2 developer community collectively curses about.
Bank sandboxes are broken. Tink spent three months testing over 100 bank sandboxes before September 2019 and concluded: the APIs are "far from ready." A large number of banks completely faked authentication steps, making it impossible to test the actual end-user journey. Sandbox environments were often built by different teams than production - integrations that worked in sandbox broke in production. Enable Banking's country assessment found Finnish sandboxes were "pretty good" but Baltic banks were "quite limited in functionality with quite many omissions making them quite far from their live counterparts." I've had banks where the sandbox URL wasn't documented anywhere - I had to email their developer support (which took 3 weeks to respond) to get it.
Every bank is a unique snowflake, even within the same standard. Berlin Group v1.3.6 is a spec. Banks interpret it. Differently. Date formats vary. Optional fields appear or disappear. Error codes are inconsistent. Documentation is sometimes local-language only. Tink's engineering team described it as "thousands of interpretations" producing "a big old mess." A clean bank API takes 2–4 days to integrate. Most take around two weeks. Some never quite work right.
SCA redirect flows are a UX nightmare. Start in your app → redirect to bank → two-factor auth → redirect back. On desktop it's tolerable. On mobile it's a disaster: browser-to-app handoffs fail, banks' mobile apps don't handle incoming auth requests correctly, users get stuck in redirect loops. Each bank has different timeout durations and error handling. I've seen redirect URIs break because the bank URL-encodes query parameters differently from the spec.
The 90-day re-authentication wall. RTS Article 10: banks may optionally exempt AIS access from SCA for 90 days, after which re-auth is mandatory. "Optionally" - some banks apply it, some don't, some use shorter intervals. For an app aggregating data from 5 banks, users face staggered re-auth prompts. Your background data refresh (4x/day per consent) just stops working on day 91 if you don't handle this. Build re-auth scheduling into your consent manager. Track per-bank, per-consent expiry. Notify users before expiry, not after.
Certificate renewal will ruin your week. Salt Edge documented this across 49 European banks. Results: 26 banks required manual developer portal login for renewal. 5 required creating entirely new applications - invalidating ALL existing user consents. 6 required new DCR with different client names. 11 had no documented renewal process at all. There are no EU-level guidelines on renewal procedures. Every bank does it differently.
My mitigation: I run a certificate renewal tracking table in PostgreSQL with per-bank renewal procedures documented as runbooks, 30-day advance alerting, and automated DCR re-registration where the bank supports it. Manual for the rest. It's ugly but it works.
PSD2 mandates the bare minimum of data. Basic account details, balances, transaction history. No spending categories, no merchant identification, no income classification, no investment holdings. If you're building anything beyond a basic account viewer, you'll need to enrich the data yourself or use an aggregator that does it for you. The regulation created access to raw plumbing, not finished products.
Conclusion
PSD2 is simultaneously one of the most important pieces of financial regulation in European history and one of the worst-executed technical mandates I've encountered. The regulatory intent was sound: break the bank monopoly on account data, enable innovation, protect consumers with strong authentication. The implementation - four competing standards, thousands of inconsistent bank APIs, operationally nightmarish certificate management, and sandboxes that don't reflect production - has created a full-employment program for fintech middleware companies and a lot of suffering for individual developers.
The structural lesson is clear: standardization requires an implementation body, not just legislation. The UK's OBIE-driven approach produced materially better APIs than the EU's "technology-neutral" approach. This isn't a subtle difference - it's the difference between integrating a bank in 2 days versus 2 weeks.
If you're building in this space today, my practical advice:
Use an aggregator for multi-market coverage unless you have a dedicated fintech engineering team of 5+. For single-market deep integrations with specific banks, go direct. Invest heavily in error handling and retry logic - bank APIs are unreliable in ways you haven't yet imagined. Build consent re-authentication as a first-class UX flow. Treat eIDAS certificate lifecycle management as critical infrastructure. And budget 3x the integration time you think you need.
The infrastructure is messy. The standards are fragmented. The bank APIs are frequently terrible. But the underlying capability - programmatic access to bank account data and payment initiation across 31 countries - is genuinely transformative. We're building on a shaky foundation, but we're building.
If you're facing similar challenges, let's talk.
Bring the current architecture context and delivery constraints, and we can map out a focused next step.
Book a Discovery CallNewsletter
Stay connected
Not ready for a call? Get the next post directly.