How to integrate with open banking APIs under PSD2, covering account information, payment initiation, and consent management.
Open banking, enabled by PSD2, gives licensed third parties access to bank account data and the ability to initiate payments directly from customer bank accounts. For developers, this means integrating with bank APIs that vary wildly in implementation quality, despite nominally following the same specifications.
This guide covers what you actually encounter when building open banking integrations.
AIS lets you read a customer's bank account data with their consent:
Use cases: Personal finance management, creditworthiness assessment, accounting integration, income verification.
PIS lets you initiate a payment from a customer's bank account:
Use cases: E-commerce checkout (pay by bank), invoice payment, account-to-account transfers.
Every open banking operation starts with customer consent. The flow follows a standard pattern but the details differ per bank:
1. Your app requests consent via the bank's API
2. Bank returns a consent URL
3. You redirect the customer to the bank's authorization page
4. Customer logs in to their bank and authorizes access
5. Bank redirects back to your app with an authorization code
6. You exchange the code for an access token
7. You use the access token to call the bank's API
This is essentially OAuth 2.0 with some PSD2-specific parameters.
Some banks support a decoupled flow where the customer authorizes in their banking app instead of a browser redirect. The flow becomes:
1. Your app requests consent
2. Bank sends a push notification to the customer's banking app
3. Customer authorizes in the app
4. Your app polls the bank's API for consent status
5. Once authorized, you receive the access token
This is better UX for mobile-first banks but adds polling complexity.
Most European banks follow (or claim to follow) the Berlin Group's NextGenPSD2 specification. Key endpoints:
POST /v1/consents # Create consent
GET /v1/consents/{consentId} # Get consent status
GET /v1/accounts # List accounts
GET /v1/accounts/{accountId} # Account details
GET /v1/accounts/{accountId}/balances # Account balances
GET /v1/accounts/{accountId}/transactions # Transaction list
POST /v1/payments/sepa-credit-transfers # Initiate payment
The specification is one thing. Bank implementations are another:
Inconsistent field names. Some banks use iban, others use IBAN or accountIban. Your deserialization must be flexible.
Missing optional fields. Fields marked "optional" in the spec are frequently absent. Never assume a field will be present.
Different date formats. ISO 8601 is the standard, but some banks return dates in local formats.
Rate limits without documentation. Banks impose rate limits that are often undocumented. Build in backoff from the start.
Sandbox versus production divergence. A bank's sandbox environment may behave differently from production. Test with real accounts before launching.
Given the inconsistency across bank APIs, most developers use an aggregator rather than connecting to banks directly. Aggregators (Plaid, Tink, Nordigen/GoCardless, Salt Edge) provide:
The trade-off is cost (per-connection or per-API-call pricing) and an additional dependency.
PSD2 consents are not permanent. They require active management:
class BankConsent extends Model
{
protected $casts = [
'granted_at' => 'datetime',
'expires_at' => 'datetime',
'revoked_at' => 'datetime',
];
public function isActive(): bool
{
return $this->revoked_at === null
&& $this->expires_at->isFuture();
}
public function needsRenewal(): bool
{
// Renew 7 days before expiry
return $this->expires_at->subDays(7)->isPast();
}
}
90-day access window: Most AIS consents expire after 90 days. After that, the customer must re-authorize. Schedule re-consent requests before expiry so users do not lose functionality unexpectedly.
Customer revocation: Customers can revoke access at their bank at any time. Your system receives no notification of this. API calls will simply start failing with 401/403 errors. Handle this gracefully and prompt the customer to re-authorize.
Store enough to manage the consent lifecycle:
Bank APIs are less reliable than you might expect. Plan for:
Bank maintenance windows. Many banks have scheduled downtime (often weekends or late nights). Retry after a delay.
SCA step-up requirements. Some banks require SCA for API access periodically. Your flow must handle being redirected to authentication unexpectedly.
Partial data. Transaction endpoints may return incomplete data during high-load periods. Compare transaction counts across calls and flag discrepancies.
Connection timeouts. Bank APIs can be slow. Set reasonable timeouts (30 seconds for data retrieval) and implement circuit breakers for persistently slow banks.
Transaction data from different banks looks different. Normalize it for your application:
class TransactionNormalizer
{
public function normalize(array $bankTransaction, string $bankId): NormalizedTransaction
{
return new NormalizedTransaction(
externalId: $this->extractId($bankTransaction, $bankId),
date: $this->parseDate($bankTransaction, $bankId),
amount: $this->parseAmount($bankTransaction, $bankId),
currency: $bankTransaction['currency'] ?? 'EUR',
description: $this->cleanDescription($bankTransaction),
counterpartyName: $this->extractCounterparty($bankTransaction),
counterpartyIban: $this->extractIban($bankTransaction),
category: null, // Enrichment happens later
);
}
}
Build bank-specific parsers behind a common interface. When a new bank does something unexpected (and they will), you only need to update one parser.
Open banking integration is rewarding but messy. Bank APIs are inconsistent, consents expire, and reliability varies. Use aggregators when the cost is justified, build robust error handling, and manage consent lifecycles proactively. The payoff is powerful financial data access that was impossible before PSD2.
Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.
Book a 30-min Call