Berlin Group NextGenPSD2 in Java: Implementing Open Banking APIs for European Compliance

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

  1. Regulatory Compliance: PSD2 mandates open banking access across the EU/EEA; Berlin Group provides the technical specification.
  2. Cross-Border Interoperability: Same API design works across multiple European countries and banks.
  3. Reduced Integration Effort: Standardized interfaces mean one integration works with hundreds of banks.
  4. Future-Proofing: Berlin Group standards evolve with market needs and regulatory changes.
  5. 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 ProfilePurposeKey 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
CommonShared 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

  1. Certificate Management: Implement automated certificate rotation and monitoring for eIDAS certificates.
  2. Error Handling: Map Berlin Group error codes to appropriate business exceptions and retry strategies.
  3. Idempotency: Use X-Request-ID headers to ensure idempotent processing of payment initiations.
  4. SCA Flow Optimization: Implement proper session management for multi-step SCA flows.
  5. Logging: Log all PSD2 interactions with sufficient detail for audit and compliance.
  6. Performance: Implement connection pooling and caching for frequently accessed data (e.g., account lists).
  7. 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/

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper