The Berlin Group NextGenPSD2 framework is the dominant standard for open banking APIs across Europe, providing a harmonized interface for banks and third-party providers (TPPs) to comply with the Revised Payment Services Directive (PSD2). For Java developers building financial applications, implementing the Berlin Group standards is essential for accessing European bank accounts, initiating payments, and retrieving account information in a compliant, interoperable manner.
What is the Berlin Group NextGenPSD2 Framework?
The Berlin Group is a pan-European payments interoperability standards body that developed the NextGenPSD2 framework—a common API standard for PSD2 compliance. It provides:
- Account Information Services (AIS): Read access to account balances and transactions
- Payment Initiation Services (PIS): Initiate credit transfers, instant payments, and other payment types
- Confirmation of Funds (CoF): Verify available funds before payment execution
- Strong Customer Authentication (SCA): Integration with redirect, decoupled, and embedded SCA flows
- Certificate Validation: eIDAS certificate handling for TPP authentication and identification
Why Berlin Group NextGenPSD2 is Critical for European Fintech
- Regulatory Compliance: PSD2 mandates open banking access across the EU/EEA; Berlin Group provides the technical specification.
- Cross-Border Interoperability: Same API design works across multiple European countries and banks.
- Reduced Integration Effort: Standardized interfaces mean one integration works with hundreds of banks.
- Future-Proofing: Berlin Group standards evolve with market needs and regulatory changes.
- Security by Design: Built on top of OAuth 2.0, FAPI, and eIDAS certificate infrastructure.
Berlin Group API Structure
The Berlin Group specification defines several API profiles:
| API Profile | Purpose | Key Endpoints |
|---|---|---|
| AIS (v2.3.x) | Account Information Services | /accounts, /balances, /transactions |
| PIS (v2.3.x) | Payment Initiation Services | /payments, /bulk-payments, /periodic-payments |
| PIIS (v2.3.x) | Confirmation of Funds | /funds-confirmations |
| Common | Shared functionality | /consents, /authorisations, /signingBasket |
Implementing a Berlin Group PSD2 Client in Java
1. Maven Dependencies
<dependencies> <!-- Spring Boot Web for REST clients --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security OAuth2 Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- eIDAS certificate handling --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>1.78</version> </dependency> <!-- JWT and OpenID Connect --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37.2</version> </dependency> <!-- Berlin Group model classes --> <dependency> <groupId>io.github.berlin-group</groupId> <artifactId>psd2-api-models</artifactId> <version>2.3.0</version> </dependency> <!-- HTTP Client with mutual TLS --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> </dependency> </dependencies>
2. eIDAS Certificate Management
@Component public class EIDASCertificateManager { private final KeyStore keyStore; private final String keyStorePassword; private final String keyPassword; public EIDASCertificateManager( @Value("${psd2.eidas.keystore.path}") String keystorePath, @Value("${psd2.eidas.keystore.password}") String keystorePassword, @Value("${psd2.eidas.key.password}") String keyPassword) throws Exception { this.keyStorePassword = keystorePassword; this.keyPassword = keyPassword; this.keyStore = KeyStore.getInstance("PKCS12"); try (FileInputStream fis = new FileInputStream(keystorePath)) { keyStore.load(fis, keystorePassword.toCharArray()); } } public X509Certificate getQsealCertificate() { try { // QSEAL: Qualified Seal Certificate for signing return (X509Certificate) keyStore.getCertificate("qseal"); } catch (KeyStoreException e) { throw new PSD2Exception("Failed to load QSEAL certificate", e); } } public X509Certificate getQwacCertificate() { try { // QWAC: Qualified Website Authentication Certificate for TLS return (X509Certificate) keyStore.getCertificate("qwac"); } catch (KeyStoreException e) { throw new PSD2Exception("Failed to load QWAC certificate", e); } } public PrivateKey getSigningKey() { try { return (PrivateKey) keyStore.getKey("qseal", keyPassword.toCharArray()); } catch (Exception e) { throw new PSD2Exception("Failed to load signing key", e); } } public String getCertificateFingerprint(X509Certificate cert) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] fingerprint = digest.digest(cert.getEncoded()); return Base64.getEncoder().encodeToString(fingerprint); } catch (Exception e) { throw new PSD2Exception("Failed to calculate certificate fingerprint", e); } } public String getCertificateSerialNumber(X509Certificate cert) { return cert.getSerialNumber().toString(16).toUpperCase(); } public String getCertificateIssuerDN(X509Certificate cert) { // Format for PSD2: CN=..., O=..., C=... return cert.getIssuerX500Principal().getName(); } } 3. Mutual TLS Configuration
@Configuration public class MutualTLSConfig { private final EIDASCertificateManager certManager; public MutualTLSConfig(EIDASCertificateManager certManager) { this.certManager = certManager; } @Bean public CloseableHttpClient psd2HttpClient() throws Exception { // Load QWAC certificate for TLS client authentication X509Certificate qwacCert = certManager.getQwacCertificate(); PrivateKey qwacKey = certManager.getSigningKey(); // Same key often used // Create keystore with QWAC KeyStore clientKeyStore = KeyStore.getInstance("PKCS12"); clientKeyStore.load(null, null); clientKeyStore.setKeyEntry("qwac", qwacKey, "changeit".toCharArray(), new java.security.cert.Certificate[]{qwacCert}); // Create truststore (include bank certificates) KeyStore trustStore = KeyStore.getInstance("PKCS12"); trustStore.load(new FileInputStream("src/main/resources/truststore.p12"), "changeit".toCharArray()); // Build SSL context SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(clientKeyStore, "changeit".toCharArray(), (aliases, socket) -> "qwac") // Key alias .loadTrustMaterial(trustStore, null) .build(); // Create HTTP client with mutual TLS return HttpClients.custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .addInterceptorFirst(new PSD2RequestInterceptor()) .build(); } @Bean public RestTemplate psd2RestTemplate(CloseableHttpClient httpClient) { HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); requestFactory.setConnectTimeout(10000); requestFactory.setConnectionRequestTimeout(10000); requestFactory.setReadTimeout(30000); RestTemplate restTemplate = new RestTemplate(requestFactory); // Add message converters for Berlin Group formats restTemplate.getMessageConverters().add(0, new BerlinGroupJsonHttpMessageConverter()); return restTemplate; } /** * Request interceptor to add PSD2-specific headers */ private static class PSD2RequestInterceptor implements HttpRequestInterceptor { @Override public void process(HttpRequest request, HttpContext context) { // Add PSD2 headers request.setHeader("PSU-IP-Address", getClientIpAddress(context)); request.setHeader("PSU-User-Agent", getClientUserAgent(context)); request.setHeader("PSU-Http-Method", request.getRequestLine().getMethod()); request.setHeader("PSU-Device-ID", getDeviceId(context)); // Add signature header (will be added by another interceptor) request.setHeader("Signature", createSignatureHeader(request)); // Add digest header for request body if (request instanceof HttpEntityEnclosingRequest) { addDigestHeader((HttpEntityEnclosingRequest) request); } } private void addDigestHeader(HttpEntityEnclosingRequest request) { try { // Calculate SHA-256 digest of request body byte[] body = EntityUtils.toByteArray(request.getEntity()); byte[] hash = MessageDigest.getInstance("SHA-256").digest(body); String digest = "SHA-256=" + Base64.getEncoder().encodeToString(hash); request.setHeader("Digest", digest); } catch (Exception e) { throw new RuntimeException("Failed to calculate digest", e); } } } } Account Information Service (AIS) Implementation
1. Consent Creation
@Service public class BerlinGroupAISService { private final RestTemplate restTemplate; private final EIDASCertificateManager certManager; private final String baseUrl; public BerlinGroupAISService( RestTemplate restTemplate, EIDASCertificateManager certManager, @Value("${psd2.ais.base-url}") String baseUrl) { this.restTemplate = restTemplate; this.certManager = certManager; this.baseUrl = baseUrl; } /** * Create a consent for account access * Berlin Group: POST /v1/consents */ public ConsentResponse createConsent(ConsentRequest request, String psuId) { HttpHeaders headers = createHeaders(psuId); // Add TPP certificate for identification headers.set("TPP-Signature-Certificate", encodeCertificate(certManager.getQsealCertificate())); // Berlin Group specific headers headers.set("Consent-ID", UUID.randomUUID().toString()); headers.set("PSU-ID", psuId); headers.set("PSU-ID-Type", "ALL"); headers.set("PSU-Corporate-ID", psuId); headers.set("PSU-Corporate-ID-Type", "ALL"); HttpEntity<ConsentRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<ConsentResponse> response = restTemplate.exchange( baseUrl + "/v1/consents", HttpMethod.POST, entity, ConsentResponse.class ); return response.getBody(); } /** * Start consent authorisation (SCA redirect) * Berlin Group: GET /v1/consents/{consentId}/authorisations */ public AuthorisationResponse startConsentAuthorisation(String consentId) { HttpHeaders headers = createHeaders(null); HttpEntity<?> entity = new HttpEntity<>(headers); ResponseEntity<AuthorisationResponse> response = restTemplate.exchange( baseUrl + "/v1/consents/{consentId}/authorisations", HttpMethod.POST, entity, AuthorisationResponse.class, consentId ); return response.getBody(); } /** * Get consent status * Berlin Group: GET /v1/consents/{consentId}/status */ public ConsentStatusResponse getConsentStatus(String consentId) { HttpHeaders headers = createHeaders(null); HttpEntity<?> entity = new HttpEntity<>(headers); ResponseEntity<ConsentStatusResponse> response = restTemplate.exchange( baseUrl + "/v1/consents/{consentId}/status", HttpMethod.GET, entity, ConsentStatusResponse.class, consentId ); return response.getBody(); } /** * Get account list * Berlin Group: GET /v1/accounts?withBalance=true */ public AccountListResponse getAccounts(String consentId, boolean withBalance) { HttpHeaders headers = createHeaders(null); headers.set("Consent-ID", consentId); HttpEntity<?> entity = new HttpEntity<>(headers); ResponseEntity<AccountListResponse> response = restTemplate.exchange( baseUrl + "/v1/accounts?withBalance={withBalance}", HttpMethod.GET, entity, AccountListResponse.class, withBalance ); return response.getBody(); } /** * Get account balances * Berlin Group: GET /v1/accounts/{accountId}/balances */ public BalancesResponse getBalances(String consentId, String accountId) { HttpHeaders headers = createHeaders(null); headers.set("Consent-ID", consentId); HttpEntity<?> entity = new HttpEntity<>(headers); ResponseEntity<BalancesResponse> response = restTemplate.exchange( baseUrl + "/v1/accounts/{accountId}/balances", HttpMethod.GET, entity, BalancesResponse.class, accountId ); return response.getBody(); } /** * Get account transactions * Berlin Group: GET /v1/accounts/{accountId}/transactions */ public TransactionsResponse getTransactions( String consentId, String accountId, LocalDate dateFrom, LocalDate dateTo, String bookingStatus) { HttpHeaders headers = createHeaders(null); headers.set("Consent-ID", consentId); HttpEntity<?> entity = new HttpEntity<>(headers); UriComponentsBuilder builder = UriComponentsBuilder .fromPath(baseUrl + "/v1/accounts/{accountId}/transactions") .queryParam("dateFrom", dateFrom.format(DateTimeFormatter.ISO_DATE)) .queryParam("dateTo", dateTo.format(DateTimeFormatter.ISO_DATE)) .queryParam("bookingStatus", bookingStatus); ResponseEntity<TransactionsResponse> response = restTemplate.exchange( builder.build().toUriString(), HttpMethod.GET, entity, TransactionsResponse.class, accountId ); return response.getBody(); } private HttpHeaders createHeaders(String psuId) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); // PSD2 required headers headers.set("TPP-Request-ID", UUID.randomUUID().toString()); headers.set("TPP-Redirect-URI", "https://tpp.example.com/callback"); headers.set("TPP-Notification-URI", "https://tpp.example.com/notify"); // PSU identification (if available) if (psuId != null) { headers.set("PSU-ID", psuId); headers.set("PSU-ID-Type", "ALL"); } // Add signature (would be calculated by interceptor) headers.set("Signature", "calculated-signature"); return headers; } private String encodeCertificate(X509Certificate cert) { try { return "-----BEGIN CERTIFICATE-----\n" + Base64.getEncoder().encodeToString(cert.getEncoded()) + "\n-----END CERTIFICATE-----"; } catch (CertificateEncodingException e) { throw new PSD2Exception("Failed to encode certificate", e); } } } 2. Consent Request Models
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class ConsentRequest { private Access access; private Boolean recurringIndicator; private LocalDate validUntil; private Integer frequencyPerDay; private Boolean combinedServiceIndicator; @Data @Builder public static class Access { private List<AccountReference> accounts; private List<AccountReference> balances; private List<AccountReference> transactions; private AvailableAccounts availableAccounts; private AllPsd2 allPsd2; } public enum AvailableAccounts { allAccounts, allAccountsWithBalances } public enum AllPsd2 { allAccounts } } @Data @Builder public class AccountReference { private String iban; private String bban; private String pan; private String maskedPan; private String msisdn; private String currency; } Payment Initiation Service (PIS) Implementation
1. Single Payment Initiation
@Service public class BerlinGroupPISService { private final RestTemplate restTemplate; private final EIDASCertificateManager certManager; private final String baseUrl; /** * Initiate a single payment * Berlin Group: POST /v1/payments/{payment-product} */ public PaymentInitiationResponse initiatePayment( PaymentRequest request, String paymentProduct, String psuId) { HttpHeaders headers = createHeaders(psuId); HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<PaymentInitiationResponse> response = restTemplate.exchange( baseUrl + "/v1/payments/{payment-product}", HttpMethod.POST, entity, PaymentInitiationResponse.class, paymentProduct ); return response.getBody(); } /** * Get payment status * Berlin Group: GET /v1/payments/{payment-product}/{paymentId}/status */ public PaymentStatusResponse getPaymentStatus( String paymentId, String paymentProduct) { HttpHeaders headers = createHeaders(null); HttpEntity<?> entity = new HttpEntity<>(headers); ResponseEntity<PaymentStatusResponse> response = restTemplate.exchange( baseUrl + "/v1/payments/{payment-product}/{paymentId}/status", HttpMethod.GET, entity, PaymentStatusResponse.class, paymentProduct, paymentId ); return response.getBody(); } /** * Start payment authorisation (SCA) * Berlin Group: POST /v1/payments/{payment-product}/{paymentId}/authorisations */ public AuthorisationResponse startPaymentAuthorisation( String paymentId, String paymentProduct) { HttpHeaders headers = createHeaders(null); HttpEntity<?> entity = new HttpEntity<>(headers); ResponseEntity<AuthorisationResponse> response = restTemplate.exchange( baseUrl + "/v1/payments/{payment-product}/{paymentId}/authorisations", HttpMethod.POST, entity, AuthorisationResponse.class, paymentProduct, paymentId ); return response.getBody(); } /** * Update PSU data for authorisation * Berlin Group: PUT /v1/payments/{payment-product}/{paymentId}/authorisations/{authorisationId} */ public AuthorisationResponse updatePsuData( String paymentId, String paymentProduct, String authorisationId, PsuDataRequest psuData) { HttpHeaders headers = createHeaders(psuData.getPsuId()); HttpEntity<PsuDataRequest> entity = new HttpEntity<>(psuData, headers); ResponseEntity<AuthorisationResponse> response = restTemplate.exchange( baseUrl + "/v1/payments/{payment-product}/{paymentId}/authorisations/{authorisationId}", HttpMethod.PUT, entity, AuthorisationResponse.class, paymentProduct, paymentId, authorisationId ); return response.getBody(); } } 2. Payment Request Models
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class PaymentRequest { private Boolean endToEndIdentification; private Amount instructedAmount; private AccountReference debtorAccount; private AccountReference creditorAccount; private String creditorAgent; private String creditorName; private Address creditorAddress; private String remittanceInformationUnstructured; private StructuredRemittance structuredRemittance; private LocalDate requestedExecutionDate; private LocalDateTime requestedExecutionTime; @Data @Builder public static class Amount { private String currency; private String amount; } @Data @Builder public static class StructuredRemittance { private String reference; private String referenceType; private String referenceIssuer; } @Data @Builder public static class Address { private String street; private String buildingNumber; private String city; private String postalCode; private String country; } } @Data @Builder public class PaymentInitiationResponse { private String transactionStatus; private String paymentId; private List<Link> links; private String scaRedirectUrl; private String scaOAuthUrl; @Data @Builder public static class Link { private String href; private String rel; private String type; } } Strong Customer Authentication (SCA) Flow
@Service public class BerlinGroupSCAHandler { private final BerlinGroupAISService aisService; private final BerlinGroupPISService pisService; /** * Complete SCA flow for consent authorisation */ public void handleConsentSCA(String consentId, String authorisationId, String psuId, String psuPassword) { // 1. Get authorisation status AuthorisationResponse authStatus = aisService.getAuthorisationStatus( consentId, authorisationId); if (authStatus.getScaStatus() == SCAStatus.RECEIVED) { // 2. Provide PSU credentials PsuDataRequest psuData = PsuDataRequest.builder() .psuId(psuId) .password(psuPassword) .build(); AuthorisationResponse updated = aisService.updatePsuData( consentId, authorisationId, psuData); if (updated.getScaStatus() == SCAStatus.PSUAUTHENTICATED) { // 3. Select SCA method if multiple available if (updated.getAvailableScaMethods() != null && updated.getAvailableScaMethods().size() > 1) { ScaMethod selectedMethod = selectScaMethod( updated.getAvailableScaMethods()); aisService.selectScaMethod( consentId, authorisationId, selectedMethod.getId()); } // 4. Start SCA process (OTP, push notification, etc.) aisService.startScaProcess(consentId, authorisationId); // 5. Wait for user confirmation (async, handled via redirect) // The TPP would typically redirect the user to the ASPSP's SCA page // and wait for callback with authorisation code } } } /** * Handle OAuth2 redirect after SCA */ public String handleRedirectCallback(String code, String state) { // Exchange code for token // This follows OAuth2 authorization code flow MultiValueMap<String, String> tokenParams = new LinkedMultiValueMap<>(); tokenParams.add("grant_type", "authorization_code"); tokenParams.add("code", code); tokenParams.add("redirect_uri", "https://tpp.example.com/callback"); tokenParams.add("client_id", "tpp-client-id"); // Add TPP certificate for client authentication // Berlin Group uses MTLS for client authentication, not client secret HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(tokenParams, headers); ResponseEntity<TokenResponse> response = restTemplate.exchange( "https://aspsp.example.com/v1/token", HttpMethod.POST, entity, TokenResponse.class ); return response.getBody().getAccessToken(); } } Confirmation of Funds Service (PIIS)
@Service public class BerlinGroupPIISService { private final RestTemplate restTemplate; /** * Confirm funds availability * Berlin Group: POST /v1/funds-confirmations */ public FundsConfirmationResponse confirmFunds(FundsConfirmationRequest request) { HttpHeaders headers = createHeaders(); HttpEntity<FundsConfirmationRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<FundsConfirmationResponse> response = restTemplate.exchange( baseUrl + "/v1/funds-confirmations", HttpMethod.POST, entity, FundsConfirmationResponse.class ); return response.getBody(); } } @Data @Builder public class FundsConfirmationRequest { private AccountReference account; private Amount instructedAmount; private String cardNumber; private String payee; } @Data public class FundsConfirmationResponse { private Boolean fundsAvailable; private String reference; } Signature Calculation (HTTP Message Signatures)
@Component public class PSD2SignatureCalculator { private final EIDASCertificateManager certManager; public String calculateSignature(HttpRequest request, byte[] body) throws Exception { PrivateKey signingKey = certManager.getSigningKey(); // Build signature string according to Berlin Group spec StringBuilder signatureBase = new StringBuilder(); // HTTP method signatureBase.append(request.getMethod()).append("\n"); // Request URI signatureBase.append(request.getURI().getPath()); if (request.getURI().getQuery() != null) { signatureBase.append("?").append(request.getURI().getQuery()); } signatureBase.append("\n"); // Host header signatureBase.append(request.getHeaders().getFirst("Host")).append("\n"); // Digest header signatureBase.append("Digest: ") .append(request.getHeaders().getFirst("Digest")) .append("\n"); // X-Request-ID header signatureBase.append("X-Request-ID: ") .append(request.getHeaders().getFirst("X-Request-ID")) .append("\n"); // Date header signatureBase.append("Date: ") .append(request.getHeaders().getFirst("Date")); // Calculate signature Signature signer = Signature.getInstance("SHA256withRSA"); signer.initSign(signingKey); signer.update(signatureBase.toString().getBytes(StandardCharsets.UTF_8)); byte[] signature = signer.sign(); // Format according to RFC 7230 return String.format( "keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"(request-target) host digest x-request-id date\",signature=\"%s\"", certManager.getCertificateIssuerDN(certManager.getQsealCertificate()), Base64.getEncoder().encodeToString(signature) ); } } Berlin Group Specific Headers and Error Handling
@ControllerAdvice public class PSD2ExceptionHandler { @ExceptionHandler(PSD2Exception.class) public ResponseEntity<ErrorResponse> handlePSD2Exception(PSD2Exception ex) { ErrorResponse error = ErrorResponse.builder() .tppMessages(Collections.singletonList( TppMessage.builder() .category(ex.getCategory()) .code(ex.getCode()) .text(ex.getMessage()) .path(ex.getPath()) .build() )) .build(); HttpHeaders headers = new HttpHeaders(); headers.set("X-Request-ID", UUID.randomUUID().toString()); return new ResponseEntity<>(error, headers, ex.getHttpStatus()); } @Data @Builder public static class ErrorResponse { private List<TppMessage> tppMessages; private String _links; } @Data @Builder public static class TppMessage { private String category; private String code; private String text; private String path; } } public enum PSD2ErrorCode { // PSU identification errors FORMAT_ERROR("FORMAT_ERROR"), PSU_CREDENTIALS_INVALID("PSU_CREDENTIALS_INVALID"), // Consent errors CONSENT_INVALID("CONSENT_INVALID"), CONSENT_EXPIRED("CONSENT_EXPIRED"), CONSENT_UNKNOWN("CONSENT_UNKNOWN"), // Payment errors PAYMENT_FAILED("PAYMENT_FAILED"), FUNDS_NOT_AVAILABLE("FUNDS_NOT_AVAILABLE"), // Technical errors SERVICE_INVALID("SERVICE_INVALID"), TIMEOUT("TIMEOUT"), CERTIFICATE_INVALID("CERTIFICATE_INVALID"); private final String code; PSD2ErrorCode(String code) { this.code = code; } } Berlin Group Client Configuration
# application-psd2.yml psd2: # Bank/ASPSP configuration aspsp: name: "Demo Bank" base-url: "https://api.bank.com/psd2/v2" authorization-url: "https://auth.bank.com/oauth2/auth" token-url: "https://auth.bank.com/oauth2/token" # TPP (Third Party Provider) configuration tpp: id: "PSDDE-BAFIN-123456" name: "Demo TPP GmbH" redirect-uri: "https://tpp.example.com/callback" notification-uri: "https://tpp.example.com/notify" # eIDAS certificates eidas: keystore: path: "classpath:certs/tpp-keystore.p12" password: "${EIDAS_KEYSTORE_PASSWORD}" qseal: alias: "qseal" qwac: alias: "qwac" # API settings ais: default-consent-days: 90 max-frequency-per-day: 4 pis: supported-payment-products: - sepa-credit-transfers - instant-sepa-credit-transfers - cross-border-credit-transfers # SCA methods sca: supported-methods: - REDIRECT - DECOUPLED - EMBEDDED preferred-method: REDIRECT Testing with Berlin Group Sandbox
@SpringBootTest @ActiveProfiles("psd2-test") public class BerlinGroupIntegrationTest { @Autowired private BerlinGroupAISService aisService; @Autowired private BerlinGroupPISService pisService; @Test void testCompleteAISFlow() { // 1. Create consent ConsentRequest consentRequest = ConsentRequest.builder() .access(ConsentRequest.Access.builder() .availableAccounts(AvailableAccounts.allAccounts) .build()) .recurringIndicator(true) .validUntil(LocalDate.now().plusDays(90)) .frequencyPerDay(4) .build(); ConsentResponse consent = aisService.createConsent( consentRequest, "PSDDE-123456"); assertThat(consent.getConsentId()).isNotEmpty(); // 2. Start authorisation AuthorisationResponse auth = aisService.startConsentAuthorisation( consent.getConsentId()); assertThat(auth.getScaStatus()).isEqualTo(SCAStatus.RECEIVED); // 3. In a real test, we would simulate SCA completion // For sandbox, we can directly use the consent // 4. Get accounts AccountListResponse accounts = aisService.getAccounts( consent.getConsentId(), true); assertThat(accounts.getAccounts()).isNotEmpty(); // 5. Get balances for first account String accountId = accounts.getAccounts().get(0).getResourceId(); BalancesResponse balances = aisService.getBalances( consent.getConsentId(), accountId); assertThat(balances.getBalances()).isNotEmpty(); // 6. Get transactions LocalDate to = LocalDate.now(); LocalDate from = to.minusDays(30); TransactionsResponse transactions = aisService.getTransactions( consent.getConsentId(), accountId, from, to, "both"); assertThat(transactions.getTransactions()).isNotNull(); } } Best Practices for Berlin Group Implementation
- Certificate Management: Implement automated certificate rotation and monitoring for eIDAS certificates.
- Error Handling: Map Berlin Group error codes to appropriate business exceptions and retry strategies.
- Idempotency: Use
X-Request-IDheaders to ensure idempotent processing of payment initiations. - SCA Flow Optimization: Implement proper session management for multi-step SCA flows.
- Logging: Log all PSD2 interactions with sufficient detail for audit and compliance.
- Performance: Implement connection pooling and caching for frequently accessed data (e.g., account lists).
- Security: Never log PSU credentials or sensitive payment data.
Conclusion
The Berlin Group NextGenPSD2 framework provides a standardized approach to implementing PSD2-compliant open banking APIs across Europe. For Java developers, implementing these standards requires careful attention to:
- eIDAS certificate handling and mutual TLS
- Berlin Group API structures for AIS, PIS, and PIIS
- Strong Customer Authentication flows
- HTTP message signatures and PSD2-specific headers
- Comprehensive error handling and compliance logging
With proper implementation, Java applications can seamlessly integrate with hundreds of European banks, providing account information, payment initiation, and fund confirmation services in a compliant, secure manner. As open banking continues to evolve, Berlin Group standards will remain central to European financial integration.
Java Programming Basics – Variables, Loops, Methods, Classes, Files & Exception Handling (Related to Java Programming)
Variables and Data Types in Java:
This topic explains how variables store data in Java and how data types define the kind of values a variable can hold, such as numbers, characters, or text. Java includes primitive types like int, double, and boolean, which are essential for storing and managing data in programs. (GeeksforGeeks)
Read more: https://macronepal.com/blog/variables-and-data-types-in-java/
Basic Input and Output in Java:
This lesson covers how Java programs receive input from users and display output using tools like Scanner for input and System.out.println() for output. These operations allow interaction between the program and the user.
Read more: https://macronepal.com/blog/basic-input-output-in-java/
Arithmetic Operations in Java:
This guide explains mathematical operations such as addition, subtraction, multiplication, and division using operators like +, -, *, and /. These operations are used to perform calculations in Java programs.
Read more: https://macronepal.com/blog/arithmetic-operations-in-java/
If-Else Statement in Java:
The if-else statement allows programs to make decisions based on conditions. It helps control program flow by executing different blocks of code depending on whether a condition is true or false.
Read more: https://macronepal.com/blog/if-else-statement-in-java/
For Loop in Java:
A for loop is used to repeat a block of code a specific number of times. It is commonly used when the number of repetitions is known in advance.
Read more: https://macronepal.com/blog/for-loop-in-java/
Method Overloading in Java:
Method overloading allows multiple methods to have the same name but different parameters. It improves code readability and flexibility by allowing similar tasks to be handled using one method name.
Read more: https://macronepal.com/blog/method-overloading-in-java-a-complete-guide/
Basic Inheritance in Java:
Inheritance is an object-oriented concept that allows one class to inherit properties and methods from another class. It promotes code reuse and helps build hierarchical class structures.
Read more: https://macronepal.com/blog/basic-inheritance-in-java-a-complete-guide/
File Writing in Java:
This topic explains how to create and write data into files using Java. File writing is commonly used to store program data permanently.
Read more: https://macronepal.com/blog/file-writing-in-java-a-complete-guide/
File Reading in Java:
File reading allows Java programs to read stored data from files. It is useful for retrieving saved information and processing it inside applications.
Read more: https://macronepal.com/blog/file-reading-in-java-a-complete-guide/
Exception Handling in Java:
Exception handling helps manage runtime errors using tools like try, catch, and finally. It prevents programs from crashing and allows safe error handling.
Read more: https://macronepal.com/blog/exception-handling-in-java-a-complete-guide/
Constructors in Java:
Constructors are special methods used to initialize objects when they are created. They help assign initial values to object variables automatically.
Read more: https://macronepal.com/blog/constructors-in-java/
Classes and Objects in Java:
Classes are blueprints used to create objects, while objects are instances of classes. These concepts form the foundation of object-oriented programming in Java.
Read more: https://macronepal.com/blog/classes-and-object-in-java/
Methods in Java:
Methods are blocks of code that perform specific tasks. They help organize programs into smaller reusable sections and improve code readability.
Read more: https://macronepal.com/blog/methods-in-java/
Arrays in Java:
Arrays store multiple values of the same type in a single variable. They are useful for handling lists of data such as numbers or names.
Read more: https://macronepal.com/blog/arrays-in-java/
While Loop in Java:
A while loop repeats a block of code as long as a given condition remains true. It is useful when the number of repetitions is not known beforehand.
Read more: https://macronepal.com/blog/while-loop-in-java/