Fintech

Open Banking API Integration: A Practical Development Guide

How to integrate with open banking APIs under PSD2, covering account information, payment initiation, and consent management.

Open Banking Beyond the Buzzword

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.

The Two Service Types

Account Information Services (AIS)

AIS lets you read a customer's bank account data with their consent:

  • Account balances (current and available)
  • Transaction history (typically 90 days, sometimes more)
  • Account details (IBAN, currency, account holder name)

Use cases: Personal finance management, creditworthiness assessment, accounting integration, income verification.

Payment Initiation Services (PIS)

PIS lets you initiate a payment from a customer's bank account:

  • Single credit transfers
  • Future-dated payments
  • Standing orders (in some implementations)

Use cases: E-commerce checkout (pay by bank), invoice payment, account-to-account transfers.

The Consent Flow

Every open banking operation starts with customer consent. The flow follows a standard pattern but the details differ per bank:

Redirect-Based Consent (Most Common)

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.

Decoupled Authentication

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.

Working With Bank APIs

The Berlin Group NextGenPSD2 Specification

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 Reality of Bank API Quality

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.

Aggregator APIs

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:

  • A single API that normalizes data from hundreds of banks
  • Pre-built consent flows
  • Ongoing maintenance of bank connections
  • Fallback mechanisms when bank APIs are unavailable

The trade-off is cost (per-connection or per-API-call pricing) and an additional dependency.

Consent Management

Consent Lifecycle

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.

Storing Consent Data

Store enough to manage the consent lifecycle:

  • Bank identifier and account IDs
  • Consent reference from the bank
  • Access and refresh tokens (encrypted)
  • Grant and expiry timestamps
  • Scope of access (balances, transactions, payment initiation)

Error Handling

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.

Data Normalization

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.

Security Considerations

  • Store bank tokens with the same care as payment credentials (encrypted at rest, restricted access)
  • Implement token refresh logic that handles expired refresh tokens gracefully
  • Log all bank API interactions for audit purposes (mask account numbers and balances in logs)
  • Rate-limit your own API to prevent abuse of bank connections through your platform
  • Implement webhook signature verification for banks that push data to you

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.

Let's talk about your fintech needs

Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.

Book a 30-min Call