OpenBanking PSD2 API Development: Building Compliant APIs for European Banks
Building PSD2-compliant APIs for European banks at Oracle Financial Services. OAuth2 flows, regulatory requirements, and cross-border integration challenges.
What Is PSD2 and Why It Matters
In January 2018, the Revised Payment Services Directive (PSD2) went into effect across the European Economic Area. The regulation forced banks to open their customer account data and payment initiation capabilities to licensed third-party providers through standardized APIs. Before PSD2, banks held a monopoly on their customers' financial data. After PSD2, if a customer consented, a fintech startup in Berlin could read that customer's transaction history from a bank in Malta and initiate payments on their behalf.
I joined Oracle Financial Services in August 2017, months before the directive's enforcement date. My job was to build the APIs that would let banks comply. Over two years, I worked on PSD2-compliant API implementations for Arbuthnot Banking Group in London, Bank of Valletta in Malta, and First Bank of Nigeria — three institutions with entirely different core banking systems, regulatory environments, and technical stacks. The work taught me more about API security, cross-border software, and the gap between regulation on paper and regulation in code than anything else in my career.
The Technical Requirements
PSD2 introduces two categories of third-party provider that banks must support:
- AISP (Account Information Service Provider): Can read account balances, transaction histories, and account details with customer consent. Think budgeting apps, credit score services, financial dashboards.
- PISP (Payment Initiation Service Provider): Can initiate payments directly from a customer's bank account. Think payment gateways that bypass card networks entirely.
For each category, the bank's API must:
- Verify the third-party provider's identity through eIDAS certificates — X.509 certificates issued by Qualified Trust Service Providers that encode the provider's authorization number and role (AISP, PISP, or both).
- Implement Strong Customer Authentication (SCA) — at minimum two of three factors: something the customer knows (PIN), something they have (phone), something they are (fingerprint).
- Manage consent explicitly — the customer must grant, and be able to revoke, access to specific accounts for a defined period. Consent expires after 90 days maximum.
- Provide account and transaction data in a standardized format.
- Log every API call for audit and regulatory reporting.
The Open Banking Implementation Entity in the UK published detailed API specifications. The Berlin Group published its own NextGenPSD2 framework. And every national regulator had opinions about edge cases. We were building against a moving target.
Building OAuth2 Flows for Banks
The authentication architecture we built at Oracle Financial Services used OAuth 2.0 with specific extensions for the banking context. Standard OAuth2 gives you authorization codes, access tokens, and refresh tokens. PSD2 adds consent objects, SCA redirects, and certificate-bound tokens on top.
Here is how a typical AISP flow worked:
- The third-party provider (TPP) sends a consent request to the bank's API, specifying which accounts and data types it wants access to.
- The bank creates a consent resource and returns a
consentIdwith statusreceived. - The TPP redirects the customer to the bank's authorization endpoint, passing the
consentId. - The bank authenticates the customer (SCA) and shows them what the TPP is requesting.
- The customer authorizes. The bank updates the consent status to
validand issues an authorization code. - The TPP exchanges the code for an access token bound to that specific consent.
- All subsequent API calls include this token. The bank validates the token, checks consent scope, and checks the TPP's eIDAS certificate on every request.
We implemented this with Spring Security OAuth2 and custom JWT handling. The token generation embedded consent scope claims directly in the JWT payload — accounts, balances, transactions — so resource servers could validate access without querying the consent store on every request.
@Bean
public JwtEncoder consentAwareEncoder(JWKSource<SecurityContext> jwkSource) {
NimbusJwtEncoder encoder = new NimbusJwtEncoder(jwkSource);
return (params) -> {
JwtClaimsSet.Builder claims = JwtClaimsSet.builder()
.claim("consent_id", params.getClaims().getClaim("consent_id"))
.claim("permissions", params.getClaims().getClaim("permissions"))
.claim("tpp_id", params.getClaims().getClaim("tpp_id"))
.expiresAt(Instant.now().plus(15, ChronoUnit.MINUTES));
return encoder.encode(JwtEncoderParameters.from(claims.build()));
};
}
The 15-minute token expiry was deliberate. Banking regulators preferred short-lived tokens with refresh capability over long-lived access tokens. We stored refresh tokens server-side in encrypted form, tied to both the consent and the TPP's certificate fingerprint.
Cross-Border Challenges
Arbuthnot Banking Group — London
Arbuthnot was our UK implementation, and the UK's Financial Conduct Authority was the most prescriptive regulator. The Open Banking UK API specification defined exact JSON schemas, error codes, pagination formats, even the HTTP status codes for specific failure scenarios. The spec ran to hundreds of pages.
The challenge with Arbuthnot was their core banking system. It ran on a mainframe with COBOL batch processes. Account data was exposed through a set of internal SOAP services that were never designed for real-time, high-frequency API calls. We had to build an intermediate caching layer that pulled account data on a schedule and served it through the REST API, while still meeting the "real-time or near-real-time" requirement in the regulation. Our solution was a 30-second polling mechanism with change-detection — if the underlying data hadn't changed, we served from cache with appropriate Last-Modified headers.
Bank of Valletta — Malta
Malta falls under the European Banking Authority's guidelines, which are broader and less prescriptive than the UK's Open Banking spec. Bank of Valletta's technical team was small — maybe eight engineers total — and they relied heavily on Oracle's OBAPI (Oracle Banking APIs) platform as their core middleware.
The Bank of Valletta project was where I spent the most time on OAuth2 implementation. Their existing authentication system was session-based with server-side state. Migrating to token-based OAuth2 meant rethinking their entire security model. I built custom Spring Security filters that could validate both legacy session cookies (for their existing web banking) and OAuth2 bearer tokens (for PSD2 endpoints) on the same infrastructure. The two auth paths coexisted for eight months during their migration.
Consent management for Valletta required a dedicated microservice. We tracked consent lifecycle — created, authorized, rejected, revoked, expired — with an event-sourced model. Every state change was an immutable event. This made regulatory audits straightforward: given a consent ID, we could reconstruct every action taken against it, by whom, and when.
First Bank of Nigeria
First Bank of Nigeria wasn't under PSD2 jurisdiction, but they wanted OpenBanking-style APIs to position themselves for Nigeria's own emerging open banking framework. The Central Bank of Nigeria was publishing draft guidelines, and First Bank wanted to be first to market.
The challenge here was entirely different from Europe. First Bank's infrastructure was distributed across data centers in Lagos and Abuja with high-latency interconnects. Their core banking system, Finacle, exposed data through a different SOAP interface than what we'd built for European banks. The response schemas, error codes, and even the concept of "account" (multi-currency accounts were the norm, not the exception) required a new set of adaptors.
I built the adaptor layer as a set of transformer classes that normalized Finacle's XML responses into the OpenBanking JSON format. Each banking entity — account, balance, transaction, beneficiary — had its own transformer with explicit mapping for every field. We couldn't afford implicit conversions or assumptions about data types. When a Nigerian Naira account had sub-accounts in USD and GBP, the transformer had to produce three separate account resources with correct currency codes and relationship references.
SOAP to REST Integration
Every bank we worked with had existing SOAP web services as their primary integration layer. OBAPI, Oracle's banking middleware, communicated with core banking systems through SOAP. Our PSD2 APIs needed to be RESTful — the regulation and market expectations both demanded JSON over HTTPS.
The translation layer sat between the REST controllers and the SOAP clients. I designed it as a set of service adaptors, each responsible for one banking domain: accounts, payments, consents, funds-confirmation.
public class AccountServiceAdaptor {
private final SoapAccountClient soapClient;
public AccountResource getAccount(String accountId, String consentId) {
// Validate consent covers this account
SoapAccountResponse soap = soapClient.fetchAccount(accountId);
return AccountMapper.toResource(soap, consentId);
}
}
The AccountMapper handled the actual field mapping — SOAP's nested XML structures flattened into the OpenBanking JSON schema. Account identifiers were particularly tricky. European accounts use IBAN, but the core banking systems stored internal account numbers. The mapper maintained a bidirectional mapping table, and we exposed IBANs externally while using internal IDs for SOAP calls.
One problem we hit repeatedly: SOAP services returned everything. A single account inquiry might return the customer's name, address, tax ID, employment status, and account details in one response. The PSD2 API could only return data covered by the active consent. So the mapper also acted as a data filter, stripping fields that the consent didn't authorize. This wasn't optional — returning data outside consent scope was a regulatory violation.
Security Considerations
Banking API security goes beyond standard OAuth2. Here is what we implemented:
Certificate-Bound Access Tokens (RFC 8705): Every access token was bound to the TPP's TLS client certificate. If a token was intercepted, it couldn't be used from a different TLS connection. The token introspection endpoint verified the certificate thumbprint against the token's cnf claim.
Mutual TLS: All TPP-to-bank communication used mTLS. The bank's API gateway validated the TPP's eIDAS certificate against the national competent authority's trust list. Expired, revoked, or unrecognized certificates were rejected before the request reached application code.
Idempotency for Payments: PISP payment initiation endpoints required an X-Idempotency-Key header. We stored these keys with a 24-hour TTL. Duplicate submissions within that window returned the original response without executing a second payment. In banking, processing a payment twice is not an inconvenience — it's potential financial loss and a regulatory incident.
Rate Limiting Per TPP: Each TPP had rate limits based on their authorization scope. AISPs could make 4 requests per second per consent. PISPs had lower throughput limits because each call could move money. We implemented this with a token bucket algorithm in Redis, keyed by TPP registration ID.
Audit Logging: Every API call — request headers (minus sensitive data), response status, consent ID, TPP ID, timestamp, and processing duration — was logged to an append-only audit store. Regulators could request these logs during examinations, and they did. The log retention policy was 5 years minimum.
Lessons from Banking API Development
Two years of building banking APIs changed how I think about API design in general.
Regulation is a feature requirement. In fintech, the regulation isn't separate from the product — it IS the product. We didn't build APIs and then make them PSD2-compliant. We read the directive first and built APIs that embodied its requirements. Every endpoint, every field, every error code had a regulatory justification. This discipline — building from requirements outward rather than code first — is something I carried into every role after Oracle.
SOAP isn't dead, it's just hiding. Behind every modern banking REST API, there's usually a SOAP service doing the real work. The adaptor pattern I developed at Oracle — clean interface, explicit mapping, no leaky abstractions — became my default approach for integrating with any legacy system. The external API should never leak the internal system's quirks.
Consent is harder than authentication. Getting a user logged in is straightforward. Managing what they've authorized, for how long, for which specific resources, with revocation and expiry — that's where the real complexity lives. Any API that handles sensitive data should think about consent as a first-class resource, not an afterthought.
Cross-border software is cross-cultural software. Working directly with banking clients in London, Malta, and Lagos meant understanding three different communication styles, three different approval processes, and three different definitions of "urgent." The technical adaptor pattern applied to the human layer too — I learned to adjust how I presented technical options based on who was in the room.
Build for the audit, not just the user. Production bugs in banking aren't just engineering problems — they're potential regulatory findings. Every design decision we made included the question: "If a regulator asks us to explain this in two years, can we?" That meant event sourcing for consent, append-only logs for API calls, and immutable records for payment initiation. The upfront cost was real. The peace of mind during audits was worth it.
Building PSD2 APIs at Oracle Financial Services was the foundation of my approach to enterprise software — secure by design, compliant by construction, and clear enough that both engineers and regulators can understand the system. If you're building in the fintech space or dealing with banking integrations, I'd be happy to talk through the challenges. Get in touch or check my full experience.
Related Articles
- Building Microservices at 130 Million Requests Per Day — From banking APIs to airline-scale microservices — the patterns that carry over and the ones that don't.
- From Startup to Acquisition — The adapter pattern I used for payment gateways at my startup was the same pattern that saved us at Oracle.
- Remote Developer Hiring Guide for EU and ASEAN Companies — What I learned about cross-border collaboration working with banks in London, Malta, and Lagos.