Documentation
Version | Date | Comment | API Release |
---|---|---|---|
2.11.0 | 11-Apr-2024 | Added Transaction Details endpoint | 1.20.1 |
2.10.1 | 29-Nov-2023 | Added section on payment references after payment updates | 1.18.0 |
2.10.1 | 29-Nov-2023 | Added section on TPP contact information registration | 1.18.0 |
2.9.6 | 11-Sep-2023 | Single SCA payment added in payment initiation | 1.17.0 |
2.9.5 | 19-Jun-2023 | Update to status of x-accept-fix headers | 1.16.1 |
2.9.4 | 31-May-2023 | Update to sandbox sections on initialization and payment | 1.16.0 |
2.9.3 | 02-May-2023 | Fee information update | 1.15.0 |
2.9.2 | 23-Mar-2023 | Information about ACCP payment status for lack of funds | 1.14.2 |
2.9.1 | 14-Feb-2023 | Description of TPP-Session-ID updated | - |
Future Breaking Changes
This section describes breaking changes that can be enabled per request if the client already supports the described functionality.
The optional functionality is controlled using a HTTP header named x-accept-fix.
At some point in the future the functionality should be expected to be made default.
To support several fixes, comma-separate them in the x-accept-fix header, eg: x-accept-fix : "longer-names, amount-as-string"
- Card-account: the value of transactionAmount and originalAmount will be switched during Q1 2021. To prepare for this, including "cardaccount-switch-originalamount-and-transactionamount" in the x-accept-fix header will enable the fix today. May not affect all card-providers.
- Card-account and account name: the max length of the name field will be extended to 70 chars in Q1 2021. To prepare for this, including "longer-names" in the x-accept-fix header will enable this fix today.
Implemented Breaking Changes
Changes implemented after 3 month notification period
x-accept-fix header value | Description | Introduced | Made Default |
---|---|---|---|
amount-as-string | Enable amount serialized as string | 11.2019 | 01-Aug-2020 |
cardaccount-switch-originalamount-and-transactionamount | Enable correct card transaction amount mapping | 05.2020 | 05-Sep-2024 |
longer-names | Enable card account name extended to 70 characters | 06.2020 | 05-Sep-2024 |
cash-account-type-iso | Enable cashAccountType ISO20022 mapping | 06.2023 | 05-Sep-2024 |
Future Deprecations
Publish Date | Item | Description | Effective Date |
---|---|---|---|
29-Aug-2024 | Removing /v1/consents endpoint | The /v1/consents has been deprecated an will be replaced by /v1/bank-offered-consents | 05-Dec-2024 |
Introducing transaction details endpoint | The API will be extended with a new transaction details endpoint that will provide additional information about the transaction (see Transaction List and Details) | 15-Aug-2024 |
Transaction List and Details
With the introduction of a dedicated transaction details endpoint the API is also able to deliver a more lightweight transaction listing endpoint. Some information currently being provided in the listing response will be moved from transactions listing response to the dedicated details endpoint.
For use cases where the TPP only needs to list transactions this will mean a better performing listing operation. When details are required the details endpoint is available to complete the listing response with additional imformation items.
Third parties will need to adapt their solutions to this by the time this behaviour is made default in the API.
Known Issues
- It is currently not possible to get the details or status of a payment initiation request for domestic-norway-credit-transfers before approving the payment.
- Due to limitation in our backend systems, GET /v1/accounts/{id}/transactions with bookingStatus=both or pending will always return all pending transactions. dateFrom, dateTo and limit cannot be used to filter pending transactions. Sandbox and card-account services is not affected by this issue.
Introduction
This document explains how you as a TPP should integrate with the Open Banking API. It describes what endpoints should be called in what order, how to sign requests and what data to send in different PSU contexts. Details on each endpoint can be found in the Developer Portal, or in the Berlin Group XS2A Framework Implementation Guidelines. The implementation guidelines should also be consulted for details on the request and response data structures of these endpoints. The API is with some notable exceptions that will be covered later in this document, based on version 1.3 of the implementation guidelines.
The system is split into two parts:
- The production environment, which has real PSU data and real transactions.
- The sandbox environment, which is a safe test environment with fake user data and simulated transactions.
Overview
- Before attempting to use any API, you need to fulfill the prerequisites
- First test your client against the sandbox environment which is available in the Developer Portal.
- Make sure you can access or create mock data.
- When in production, the client must use the production environment.
- AISPs should start by reading the Account Information API section
- PISPs should start by reading the Payment Initiation API section
- There are quick start examples for both AISPs and PISPs.
Prerequisites
- Before you can use the Account Information API, you must be a registered AISP.
- Before you can use the Payment Initiation API, you must be a registered PISP.
- You need to obtain an eIDAS certificate from a trusted CA.
Your eIDAS certificate will be validated, and your TPP registration will be checked against central registries when you make requests to the Production APIs.
Register TPP Contact Email
We encourage you as a TPP operating towards the API to register your contact email for technical and operational notifications with us. This can be registered and updated using the TPP Dialogue API. We strongly suggest using only non-personal email addresses for this purpose. Note that this is not a requirement for using the API, but ensure you get important notifications as soon as possible.
Mock Data
See Sandbox API for Dynamic Mock Data for how to use the dynamic mock data API.
For basic initialization of the Sandbox with customer and account data please refer to the "Mock Data Common API". Once initialized the Sandbox resources are ready to be operated on using the full set of Berlin Group API operations.
It will create a starting point for working with the Sandbox as follows
- Private customers with 2 accounts and a card account
- Private Customer 1 SSN: 13039319955
- Private Customer 2 SSN: 12085592767
- Corporate customer with 2 accounts
- Corporate Customer 1 SSN: 18129215603
- Corporate Customer 2 SSN: 20079518612
These customers can then be accessed using 'GET /v1/sandbox/customers' to perform further operations on their agreements. This can be used instead of, or in combination with the other mock data operations that create or update individual resources.
Note that the initialization operation will reset the Sandbox to a known state, as described above. This also includes customers and accounts created manually using the other Mock Data APIs.
You can also find a sample script for custom populating the sandbox with mock data here: PSD2 Developer Sample Code
Basics
This section presents and discusses some basic concepts and features found by the Open Banking API
Open Banking Actors
Actor | Abbreviation | Type | Description |
---|---|---|---|
Payment Service User | PSU | Person | A natural or legal person making use of a payment service as a payee, payer or both (PSD2 Article 4(10)). |
Payment Service Provider | PSP | Legal Entity | A legal entity (and some natural persons) that provide payment services as defined by PSD2 Article 4(11). |
Account Servicing Payment Service Provider | ASPSP | Legal Entity | An ASPSP is a PSP that provides and maintains a payment account for a payment services user (PSD 2 Article 4(15). |
Third Party Provider | TPP | Legal Entity | A party other than an ASPSP that provides payment related services. The term is not actually defined in PSD2, but is generally deemed to include all payment service providers that are 3rd parties (the ASPSP and the PSU to whom the account belongs being the first two parties). |
Payment Initiation Service Provider | PISP | Legal Entity | A TPP that provides Payment Initiation Services. PSD2 does not offer a formal definition. Article 4(18) defines a PISP as a PSP that provides Payment Initiation Services. |
Account Information Service Provider | AISP | Legal Entity | A TPP that provides Account Information Services. PSD2 defines AISPs in Article 4(19) as a PSP that provides account information services. |
RESTful Principles
The Open Banking API adheres to RESTful API concepts where possible and sensible to do so. However, the priority is to have an API that is simple to understand and easy to use. In instances where following RESTful principles would be convoluted and complex, the principles have not been followed.
Accepted Data Encoding
The Open Banking API encodes all data as JSON. Different XML formats also mentioned in Berlin Group XS2A Framework Implementation Guidelines is not supported, but may be supported in the future.
Character Encoding
The API requests and responses must use a UTF-8 character encoding, as is the default for JSON text in RFC 7158 - Section 8.1. However, an ASPSP's downstream system may not accept some UTF-8 characters, such as emojis. If the ASPSP rejects the request with a message that a UTF-8 character cannot be processed, the ASPSP should respond with a 400 (Bad Request).
Date Formats
All dates in the JSON payloads are represented in ISO 8601 date or date time format.
ISO 8601 Date
2018-05-17
ISO 8601 DateTime
2018-05-17T08:37:12+00:00
All dates in the HTTP headers are represented as RFC 7231 Full Dates e.g.
Sun, 17 May 2018 08:37:12 UTC
TPP Redirects
The TPP needs to implement some endpoints in order to receive the query parameters in redirects from the ASPSP. Currently, the only redirects happens after the PSU has completed the SCA process. The URL for these redirects must always be provided by the TPP in the `TPP-Redirect-URI` header in case SCA is required.
Redirect after successful SCA process
Example:
https://some-aisp-with-callback.com/callback.html?psu-id=dfd07c50-20f5-4703-b5dc-ce141a13ad76&tpp-session-id=76599984&context=PRIVATE
Parameter Name | Type | Description |
---|---|---|
psu-id | RFC 4122 UID (UUID) | Identifies a PSU, it is unique for a given combination of TPP and ASPSP, and does not change. The value here should be provided in the `PSU-ID` header for all subsequent requests, and should be persisted by the TPP for future requests for the same PSU. The value of `PSU-ID` need not be shared with the PSU. |
psu-corporate-id (optional) | RFC 4122 UID (UUID) | If the PSU chooses a corporate context this field will identify one corporate agreement belonging to the PSU (there could be multiple), and should be treated the same as the PSU-ID, but set to the `PSU-CORPORATE-ID` header instead. |
tpp-session-id | String | The value of the `TPP-Session-ID` header used by the request that triggered the SCA process. |
context | String, one of (PRIVATE, CORPORATE) | Indicates whether a PSU selected a PRIVATE or CORPORATE context. See corporate context. |
Redirect after failed SCA process
Example:
http://some-aisp-with-callback.com/callback.html?system=ERA-PSD2&status=400&code=PAYMENT_FAILED&message=SomeUrlEncodedMessage
Parameter Name | type | Description |
---|---|---|
system | String | The name of the internal system that produced the error in case of a server error |
status | Number | HTTP status code |
code | String | Error code detailing the status |
message | String | URL-encoded message, detailing the error |
See Error Codes and Responses for details on error codes.
Security & Access Control
Transport Security
The communication between the TPP and the ASPSP is always secured by using a TLS-connection using TLS version 1.2 or higher.
Non-Repudiation
Digital signatures as described by draft-cavage-signatures-09 are used to facilitate non-repudiation for Open Banking API requests.
In addition, digital signatures must adhere to requirements as described in XS2A Framework Implementation Guidelines, Section 11.
Signing Specification
The TPP must sign each API request.
The TPP must provide correct Date HTTP header. The API allows connecting clients to have a maximum deviation of 60 seconds.
The ASPSP should verify the signature of API requests it receives before carrying out the request. If the signature fails validation, the ASPSP must respond with a 400 (Bad Request).
The ASPSP must reject any API requests that should be signed but do not contain a signature in the HTTP header with a 400 (Bad Request).
The ASPSP may sign the HTTP body of each API response that it produces which has an HTTP body.
The TPP should verify the signature of API responses that it receives.
Signing Process
The process of signing HTTP messages is subject to change when the draft-cavage-http-signatures-09 and ETSI TS 119 495 standards release a final version of their specification.
The target url and the required header fields must be used to sign the request. The signature is contained in its entirety in the "Signature" header field.
The 4 parts of a Signature header
Name | Description |
---|---|
keyId | The id of the key used to sign the request (in case of rsa-sha256 this points to a RSA Private Key) |
algorithm | The algorithm used for signing (for example rsa-sha256) |
headers | Contains all the required header names separated by space in the same order they will be concatenated for signing |
target url | Which is not a header is indicated with (request-target) |
signature | The signature string, see below |
Requirements for Key ID
It consists of two parts SN= and CA=
- SN should contain the hex encoded upper case representation of the serial number of the certificate.
- CA should contain The issuer of the certificate. This should be formatted according to RFC 2253.
Required Headers for Signature
The headers listed below should be put in the signature if they are used in the request.
- date
- digest
- x-request-id
- psu-id
- psu-corporate-id
- tpp-redirect-uri
Signature Requirements
- If a header is not included in the request, it should not be included in the signature
- All header names must be lowercase.
- Concatenate header names and values with a separator of ":" and a space.
- Separate each header with a newline
- Remember to use the same headers in the same ordering as the "headers" value
- Sign this string using the defined algorithm, and the key defined by keyId.
- Base64 encode the resulting bytes.
Example of how to concatenate header fields before signing
first-header-name: first-header-value
second-header-name: second-header-value
After this concatenate the 4 parts of the Signature header in order
- Format each part: key="value"
- Concatenate each part using ',' (comma)
Example of a completed Signature header
Assume the `algorithm` value was "rsa-sha256". This would signal to the application that the data associated with the `keyId` is an RSA Private Key, the signature string hashing function is SHA-256, and the signing algorithm is the one defined in Section 8.2.1 of RFC 3447. The result of the signature creation algorithm should result in a binary string, which is then base 64 encoded and placed into the `signature` value, currently indicated as "base64(rsa-sha256(signing-string))".
Signature: keyId="SN=DCC5CCB85FDAB32A,CA=O=PSDNO-FSA-SOMEASPSP,L=Trondheim,C=NO,CA=", algorithm="rsa-sha256", headers="digest tpp-transaction-id tpp-request-id psu-id date", signature="base64(rsa-sha256(signing-string))"
ISO-8859-1 or UTF-8 encoded headers
If any values in the Signature header is ISO-8859-1 or UTF-8 encoded you need to URL encode the Signature header according to RFC 2047 which means MIME encoding the signature.
Also, the signature must be wrapped using this format: =?charset?encoding?encoded signature?=
Signature example with this encoding:
Signature: =?utf-8?B?a2V5QTQsQ0E9Mi41LjQuOTc9IzB........jMTM1MDUzNDQ0ZTRmMmQ0NjUz?=
Java example of how to implement encoding:
if (charset.equals(StandardCharsets.UTF_8)) {
Signature = String.format("=?utf-8?B?%s?=", Base64.getEncoder().encodeToString(signature.getBytes(StandardCharsets.UTF_8)));
}
Strong Customer Authentication
SCA is mandated by the RTS and supported by the Open Banking API using the SCA redirect flow as detailed in XS2A Framework Implementation Guidelines, Section 5.1. In accordance with RTS Chapter III the API may exercise the option of exempting a transaction from SCA. However, based on transaction risk analysis and other factors described in the RTS the API may also require SCA on any transaction. The TPP application must always inspect the response for an scaRedirect link to identify the SCA requirements of the current transaction.
The SCA flow have two steps;
- requests that require SCA will be responded to with a scaRedirect link to the ASPSP SCA authorisation site
- after the SCA process is complete the ASPSP will redirect the PSU to the URI provided by the TPP in the `TPP-Redirect-URI` header
Note that the `TPP-Redirect-URI` header is required for all requests, as the TPP has no prior knowledge of when a PSU will be required to complete the SCA process.
During the SCA process the ASPSP will automatically gather consent, and the TPP will for subsequent request only be granted access to interact with these consented accounts.
An SCA is valid for a single TPP and PSU, but across TPP roles (such as PISP and AISP), as governed by `TPP-Session-ID`. Such sessions must be managed in accordance with the PSD2 regulative.
Following a successful SCA process the PSU will be redirected to the `TPP-Redirect-URI` which should be a TPP endpoint as described in TPP Redirects
TPP-Session-ID
Using the TPP-Session-ID facility TPPs can operate without further SCAs withing the following constraints.
- An SCA within a given
TPP-Session-ID
will last for up to 1 hour before expiring. - The use of
TPP-Session-ID
will prevent the API from prompting for new SCA during the 1-hour time window, when calling PSD2 API services requiring SCA. - The SCA exemption for this 1-hour window does not apply to services that require SCA with dynamic linking.
- This functionality, including time limits, is identical to the standard internet bank.
- When expired, an SCA is required to renew it.
- Creating a new
TPP-Session-ID
before an old one is expired will require a new SCA, and if completed will invalidate the oldTPP-Session-ID
. If the SCA is aborted, the oldTPP-Session-ID
will still be valid.
Sandbox and Production
- The user interface in production is similar to the other ASPSP channels. If it is the first time a TPP asks for access to accounts, the PSU must confirm granting access before selecting SCA method and performing SCA.
- The user interface in the sandbox environment is simplified to allow for testing mocked PSUs. Only a mocked national identification number is required here.
- The SCA flow as seen from a TPP is the same for both sandbox and production.
Biometric Authentication for Mobile Clients
In production biometric authentication is possible using the ASPSP mobile application. To directly trigger biometric authentication for mobile TPP apps where available send the following header and value with every request:
TPP-Redirect-Preferred: False
Consent
This API uses the Bank Offered Consent approach described in XS2A Framework Implementation Guidelines, section 6.4 It means that the PSU will give consent during the SCA redirect or directly with the ASPSP, and the POST to /v1/consents will disregard the body and only return a sca redirect.
Consents are granted by a PSU to a TPP for accessing details, balances and transactions on accounts held with the ASPSP. This means that a TPP do not have access to any account information from a PSU until access has been explicitly granted.
The consent flow is explained further under Account Information API.
Bank Offered Consent User Interface
This is a description of the ASPSP user interface the PSU will see, and it does not directly affect the TPP. This flow is the same in sandbox and production.
- When granting consent the PSU will first need to perform SCA as described under "Strong Customer Authentication".
- Then it will be presented with a list of selectable accounts (checkbox list)
- In case the PSU has already granted consent, accounts which the TPP has been given access to are pre-selected.
- The PSU then deselects the accounts it wants to revoke consent for, and selects the accounts it wants to grant consents for.
- The PSU then confirms and gets redirected to the TPP.
Consent Revocation
A PSU can revoke consent for accessing account information at any point in time.
The PSU can revoke authorisation directly with the ASPSP. The mechanisms for this are in the competitive space and are up to each ASPSP to implement in the ASPSP's banking interface. ASPSPs are under no obligation to notify TPPs regarding the revocation of authorisation.
Note: An ASPSP may remove payment permissions for a PSU and payment account even if that account is available in the Account Information API.
Private and Corporate Context
A PSU can act as a private person or as a representative of a corporation.
- When the PSU is a private person, it has access to its own PSD2 enabled accounts in the ASPSP
- When the PSU is a representative of a corporation, it can only access accounts that it has been granted access to.
The PSU chooses which context it will use during the SCA process. The steps in this document describes the flow for a private person. The corporate context is described here.
Special Handling for Corporate Context
The main difference is that after SCA both `PSU-ID` and `PSU-CORPORATE-ID` headers need to be set for future calls to the API.
The `PSU-CORPORATE-ID` is acquired in two ways:
1. If the TPP has not previously acquired a PSU-ID
The PSU needs to go through the SCA process and choose corporate context which can be initiated in one of two ways:
- Consent SCA flow for AISPs.
- GET /v1/user SCA flow in Payment Integration API for PISPs that are not AISPs.
After the SCA process, the PSU-CORPORATE-ID will be provided in the TPP redirect.
2. If the TPP has already acquired a PSU-ID
The set of PSU corporate agreements can be fetched by calling GET /v1/agreements. These agreements can be used by the PSU to choose its corporate context which should be set as the `PSU-CORPORATE-ID` header.
Example:
- The TPP calls GET /v1/agreements and presents them to the PSU
- The PSU selects a new corporate agreement where no consents are given.
- The TPP calls GET /v1/accounts on this context by setting the `PSU-CORPORATE-ID` header.
A new consent flow will be triggered like in Account Information API Steps The PSU would not be required to select corporate agreement in the Consent SCA process, because it is already selected by setting the `PSU-CORPORATE-ID` header in the previous call.
Account Information API
This API allows TPPs to access the accounts of a PSU and see account numbers, balances and transactions.
- Every sandbox and production request must be signed, this means the request need to contain the header `Signature`, see Signing Process.
- Every request must have the header `TPP-Redirect-URI`. This should point to an endpoint in your application, the TPP Redirect Endpoint
- The `TPP-Session-ID` is optional and can be used to correlate a redirect to the original request.
Below you will find detailed steps for doing your first request.
Quick Start Accounts Full Java Example
This is a full example showing how to get accounts, payment uses the same signing algorithm but with extra headers. You need Java 11 for this example to work.
package com.example.your.application;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Check the prerequisites before running this sample.
*
* This code requires OkHttp and Bouncy castle as dependencies
*
* Update variables with relevant values and run the main method to test.
*
* Maven dependencies:
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15on</artifactId>
* <version>1.61</version>
* </dependency>
* <dependency>
* <groupId>com.squareup.okhttp3</groupId>
* <artifactId>okhttp</artifactId>
* <version>3.13.1</version>
* </dependency>
*/
public class Psd2DEMOClient {
private static String[] signatureHeaders = {"date", "digest", "x-request-id", "psu-id", "psu-corporate-id", "tpp-redirect-uri"};
public static void main(String[] args) throws Exception {
OkHttpClient httpClient = createOkHttpClient();
/*
See the getAccount method description for information on the different fields and the sample values set here.
*/
String certificateFile = "cert.pem";
String keyFile = "cert.pkcs8.key";
/*
set this to a constant value or change it to the tpp-session-id query parameter value after following the redirect.
*/
String tppSessionId = UUID.randomUUID().toString()+"-codesample";
/*
set this one after following the redirect and run it again
*/
String psuId = null;
URL url = new URL("----- ASPSP BASE URL WITH TRAILING '/' ----");
Request request = Psd2DEMOClient.getAccounts(certificateFile, keyFile, url, tppSessionId, psuId);
Response response = httpClient.newCall(request).execute();
System.out.println("---------- Request ----------");
System.out.println(request.url());
System.out.println(request.headers());
System.out.println("---------- Response ----------");
String body = response.body().string();
System.out.println(response.code());
System.out.println(response.headers().toString());
System.out.println(body);
System.out.println("\n------------------------------------------\n");
if(body.contains("accounts")) System.out.println("Yay you got accounts!");
else if(response.code() == 200) System.out.println("Yay! It seems like it worked, but no accounts were found.");
else if(body.contains("scaRedirect")) System.out.println("Yay! you got through, now follow the scaRedirect link and then try again using the psu-id and tpp-session-id from the url parameters in the final redirect.");
else System.out.println("Something went wrong! Check the response message for details.");
}
/**
* Make sure your certificate is registered with the ASPSP.
*
* When this method has been executed successfully the body of the response (as a string) should contain a _link with a scaRedirect and a href.
* 1. Open the url in your browser.
* 2. Follow the SCA flow in the browser
* 3. After final redirect you will see a set of query parameters in the url in your browser. (example: https://httpbin.org/?psu-id=98256c61-9cd3-4244-b721-434738c63670&tpp-session-id=8b2d1322-94d7-4aee-998f-c8cdb1021e2a&context=PRIVATE)
* 4. Use the PSU-ID and Tpp-session-id url parameter values for subsequent requests.
*
* @param certificateFile path to the certificate pem file.
* @param keyFile path to the certificate key file.
* @param baseUrl ASPSP url with a trailing '/'(url to the sandbox environment excluding the path v1/accounts)
* @param tppSessionId any string (for example a UUID string) it identifies the session. The same id should be used for every request in a PSU session
* @param psuId optional psu id, after
*
*/
public static Request getAccounts(String certificateFile, String keyFile, URL baseUrl, String tppSessionId, String psuId) throws Exception {
/*
Append the API path to the API base url
*/
URL url = new URL(baseUrl,"v1/accounts");
/*
Request id to identify the request across services. Store this id and its audit trail!
*/
String requestId = UUID.randomUUID().toString();
/*
Read the certificate
*/
X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(readResourceFile(certificateFile)));
/*
Read the certificate key
*/
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(readResourceFile(keyFile)));
System.out.println("Private Key > Format: " + privateKey.getFormat());
System.out.println("Private Key > Algorithm: " + privateKey.getAlgorithm());
/*
Base 64 encode the certificate for use in the header
*/
String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded());
/*
Create headers to use for both the signature and the http request
*/
Map<String, String> headers = new LinkedHashMap<String, String>(Map.of(
"X-Request-ID", requestId,
"Date", ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME),
"TPP-Session-ID", tppSessionId,
"TPP-Redirect-URI", "https://httpbin.org",
"TPP-Signature-Certificate", encodedCertificate,
"PSU-IP-Address", "0.0.0.0" //Use an actual PSU ip address here!
));
if(psuId != null) {
headers.put("PSU-ID", psuId);
}
/*
Make all headers lower case.
*/
Map<String, String> normalizedHeaders = headers
.entrySet()
.stream()
.collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue));
/*
Create signature.
*/
String signature = createSignature(certificate, privateKey, normalizedHeaders);
/*
Create a request and set all headers including the Signature header.
*/
Headers.Builder headerBuilder = new Headers.Builder();
normalizedHeaders.forEach(headerBuilder::add);
return new Request.Builder().url(url)
.headers(headerBuilder.build())
.header("Signature", signature)
.build();
}
/**
*
* @param certificate Valid X509 certificate registered on a TPP
* @param privateKey The Private key of the x509 certificate
* @param requestHeaders
* @return The signature
* @throws Exception
*/
private static String createSignature(X509Certificate certificate, PrivateKey privateKey, Map<String, String> requestHeaders) throws Exception {
/*
Certificate serial number on upper case hex form.
Example output: 6AEB4444FBAAD267
*/
String upperCaseHEXSerialNumber = certificate.getSerialNumber().toString(16).toUpperCase().trim();
/*
Create the keyId value by Concatenating the serial and issuer name items using SN=***,CA=***
(Note that "CA=" is not part of the issuer name and "SN=" is not part of the serial)
Example output:
SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO
*/
String keyId = String.format("SN=%s,CA=%s",
upperCaseHEXSerialNumber,
certificate.getIssuerX500Principal().getName(X500Principal.RFC2253));
System.out.println("Signature > Key ID: " + keyId);
/*
Filter the headers so only the ones needed for the request are left.
*/
List<String> headersForSigning = requestHeaders.entrySet().stream()
.map(Map.Entry::getKey)
.filter(headerName -> Arrays.asList(signatureHeaders).contains(headerName))
.collect(Collectors.toList());
/*
Create the headers value by concatenating the header names (Concatenate with whitespace *not* comma)
This is used to communicate the header ordering to the ASPSP for signature validation.
*/
String headers = String.join(" ", headersForSigning);
/*
Concatenate all the headers to make the signing string.
They are concatenate in this way:
name1: value1
name2: value2
*/
String signingString = headersForSigning.stream()
.map( headerName -> String.format("%s: %s", headerName, requestHeaders.get(headerName)))
.collect(Collectors.joining("\n"));
/*
Sign the string using RSA SHA256.
The output is a byte array
*/
Signature sha256withRSA = Signature.getInstance("SHA256withRSA", new BouncyCastleProvider());
sha256withRSA.initSign(privateKey);
sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
byte[] signedBytes = sha256withRSA.sign();
/*
Validate signature (this is not strictly neccessary, the purpose in this example is to provide error messages
if there is a problem with the input certificate or key).
*/
/*
Base 64 encode the byte result of the signing algorithm to get a string.
*/
String base64EncodedSignedBytes = Base64.getEncoder().encodeToString(signedBytes);
sha256withRSA.initVerify(certificate.getPublicKey());
sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
boolean valid = sha256withRSA.verify(signedBytes);
if (valid) {
System.out.println(String.format("Signature > VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
} else {
System.out.println(String.format("Signature > NOT VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
throw new SignatureException(String.format("Signed bytes is signed using a private key not matching the certificate with serial %s", upperCaseHEXSerialNumber));
}
/*
Create the Signature header value by concatenating the keyId value, headers value, algorithm used and the base 64 encoded signature.
Example output: keyId="SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO", algorithm="rsa-sha256", headers="date x-request-id tpp-redirect-uri psu-id", signature="***************"
*/
String signature = String.format("keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"%s\",signature=\"%s\"", keyId, headers, base64EncodedSignedBytes);
return signature;
}
private static OkHttpClient createOkHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(60 * 1000, TimeUnit.MILLISECONDS)
.readTimeout(60 * 1000, TimeUnit.MILLISECONDS)
.writeTimeout(60 * 1000, TimeUnit.MILLISECONDS)
.build();
}
public static byte[] readResourceFile(String fileName) throws IOException {
Psd2DEMOClient.class.getResource(fileName);
try (InputStream inputStream = MethodHandles.lookup().lookupClass().getResourceAsStream(fileName)) {
return inputStream.readAllBytes();
}
}
}
Account Information API Access
To get access to account information you need to perform minimum these steps. (These steps are for a private context, but corporate context is very similar. See Private and Corporate Context)
- Do a GET to /v1/accounts (or any other resource you want to access)
- This will return a 401 Unauthorized data containing a scaRedirect link.
- Redirect the PSU using scaRedirect, or present the link in an iframe to start the SCA flow.
- The PSU may now give consent to some or all of its accounts.
- The PSU will then be validated against the ASPSP and consent will be registered directly in the SCA flow, completely hidden from the TPP.
- After completing the SCA flow the PSU will be redirected to the url provided in the `TPP-Redirect-URI`
- The redirect will contain a PSU-ID which must be used on consecutive calls to the API
- Persist the PSU-ID, it should be used in all future requests for this PSU.
- You may now call the same resource as in the first step again (GET /v1/accounts).
- Remember to add the `PSU-ID` header using the psu-id you got in the redirect.
- For GET /v1/accounts you will now get a list of all accounts the PSU has given consent to.
After these steps the other account resources can be accessed the same way as GET /v1/accounts in the last step, even after the user session ends (with some limits. See Request Limits).
Account Information Sequence Diagram
Account Transactions
Transactions on an account are grouped into pending and booked transactions. A transaction that is presented as booked has been fully processed, while pending transactions are subject to further processing. The final processing of a transaction may change some of its properties, and some properties are first assigned in the final processing stages. For this reason certain properties of pending transactions may change between read requests.
For booked transactions specifically, the transactionId can be expected to be stable.
Additional proprietary references related to the transaction will be provided in the property additionalInformationProprietaryX when available. This information is only provided if and when additional information is available for the transaction in question.
User Not Present - Request Limits
The header `PSU-IP-Address` must be present if the PSU has asked for this account access in real-time.
`PSU-IP-Address` shall be the ip address of the device the PSU is using to access the TPP service.
If the TPP want to make a request without a PSU present, the `PSU-IP-Address` shall not be set.
There are two cases where a TPP can make requests without the `PSU-IP-Address`, and that does not generally require SCA.
However, some restrictions apply:
- GET /v1/accounts/{id}/balances is limited to
- No more than 4 calls per 24-hour period
- Accounts for which the PSU has granted consent
- GET /v1/accounts/{id}/transactions is limited to
- No more than 4 calls per 24-hour period
- Transactions no more than 90 days old
- Accounts which the PSU has accessed online, triggering an SCA within the last 180 days
Clarifications
Account and balance currency
GET /v1/accounts/{id}/balances response contain both the account currency and a currency for each balanceType, the currency of the balanceTypes will always match the account currency.
Payment Initiation API
This API allows TPPs to initiate payments on behalf of a PSU.
- Every sandbox and production request must be signed, this means the request need to contain the header `Signature`, see Signing Process.
- Every request must have the header `TPP-Redirect-URI`. This should point to an endpoint in your application, see TPP Redirect Endpoint
- The `TPP-Session-ID` is optional and can be used to correlate a redirect to the original request.
Below you will find detailed steps for doing your first request.
Quick Start Payment Full Java Example
This is a full example showing how to get accounts, payment uses the same signing algorithm but with extra headers. You need Java 11 for this example to work.
package com.example.your.applicatoion;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Check the prerequisites before running this sample.
*
* This code requires OkHttp and Bouncy castle as dependencies
*
* Update variables with relevant values and run the main method to test.
*
* Maven dependencies:
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15on</artifactId>
* <version>1.61</version>
* </dependency>
* <dependency>
* <groupId>com.squareup.okhttp3</groupId>
* <artifactId>okhttp</artifactId>
* <version>3.13.1</version>
* </dependency>
*/
public class Psd2PaymentDEMOClient {
private static String[] signatureHeaders = {"date", "digest", "x-request-id", "psu-id", "psu-corporate-id", "tpp-redirect-uri"};
public static void main(String[] args) throws Exception {
OkHttpClient httpClient = createOkHttpClient();
/*
See the performPaymentFlow method description for information on the different fields and the sample values set here.
*/
String certificateFile = "cert.pem";
String keyFile = "cert.pkcs8.key";
/*
The account numbers to use in a test payment.
Remember to change these to accounts in the test data set.
*/
String debtorAccountBban = "97105048304";
String creditorAccountBban = "97105048304";
/*
Set this to a constant value or change it to the tpp-session-id query parameter value after following the redirect.
*/
String tppSessionId = UUID.randomUUID().toString()+"-codesample";
/*
Set this to an exsisting sca-redirect
*/
String psuId = null;
/*
Set this to the content of the href in the "startAuthorisation" link in the response of a payment initiation
*/
String startAuthorisationLink = null;
URL url = new URL("----- ASPSP BASE URL WITH TRAILING '/' ----");
Request request = Psd2PaymentDEMOClient.performPaymentFlow(certificateFile, keyFile, url, debtorAccountBban, creditorAccountBban, tppSessionId, psuId, startAuthorisationLink);
Response response = httpClient.newCall(request).execute();
System.out.println("---------- Request ----------");
System.out.println(request.url());
System.out.println(request.headers());
System.out.println("---------- Response ----------");
String body = response.body().string();
System.out.println(response.code());
System.out.println(response.headers());
System.out.println(body);
System.out.println("\n------------------------------------------\n");
if(body.contains("startAuthorisation")) System.out.println("You have intiated a payment, to authorise it paste the startAuthorisation link in the example");
else if(body.contains("scaRedirect")) {
if(startAuthorisationLink == null) {
System.out.println("You got through, now follow the scaRedirect link and then try again using the psu-id and tpp-session-id from the url parameters in the final redirect.");
} else {
System.out.println("Now follow the scaRedirect link, if you get no errors after performing SCA the payment has been successfully authorised.");
}
}
else System.out.println("Something went wrong! Check the response message for details.");
}
/**
* Make sure your certificate is registered with the ASPSP.
*
*
* After this method is run without psuId or with startAuthorisationLink the body of the response (as a string) should contain a _link with a scaRedirect and a href.
* 1. Open the url in your browser.
* 2. Follow the SCA flow in the browser
* 3. After final redirect you will see a set of query parameters in the url in your browser. (example: https://httpbin.org/?psu-id=98256c61-9cd3-4244-b721-434738c63670&tpp-session-id=8b2d1322-94d7-4aee-998f-c8cdb1021e2a&context=PRIVATE)
* 4.
* a. If psuId is not present Use the PSU-ID and Tpp-session-id url parameter values for subsequent requests.
* b. if psuId and startAuthorisationLink is present these parameters without any error message indicate a successfully approved payment.
*
* @param certificateFile path to the certificate pem file.
* @param keyFile path to the certificate key file.
* @param url ASPSP url with a trailing '/'(url to the sandbox environment excluding the path v1/payments).
* @param debtorAccountBban Debtor account, will be inserted into sample json.
* @param creditorAccountBban Creditor account, will be inserted into sample json.
* @param tppSessionId any string (for example a UUID string) it identifies the session. The same id should be used for every request in a PSU session.
* @param psuId optional psu id, if present will attempt to initialize payment.
* @param startAuthorisationLink optional startAuthorisation link to follow, if present will attempt to start autorisation of the payment.
*
*/
public static Request performPaymentFlow(String certificateFile, String keyFile, URL url, String debtorAccountBban, String creditorAccountBban, String tppSessionId, String psuId, String startAuthorisationLink) throws Exception {
/*
Read the certificate
*/
X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(readResourceFile(certificateFile)));
/*
Read the certificate key
*/
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(readResourceFile(keyFile)));
System.out.println("Private Key > Format: " + privateKey.getFormat());
System.out.println("Private Key > Algorithm: " + privateKey.getAlgorithm());
/*
Step 1: If you have no PSU-ID one must be generated first.
*/
if(psuId == null) {
return getPSUId(certificate, privateKey, url,tppSessionId);
}
/*
Step 2: Initiate Payment
*/
else if(startAuthorisationLink == null) {
return initiatePayment(certificate, privateKey, url, debtorAccountBban, creditorAccountBban, tppSessionId, psuId);
}
/*
Step 3: Start payment authorisation
*/
else {
return startAuthorisation(certificate,privateKey, url, tppSessionId, psuId, startAuthorisationLink);
}
}
public static Request getPSUId(X509Certificate certificate, PrivateKey privateKey, URL baseUrl, String tppSessionId) throws Exception {
/*
Append the API path to the API base url
*/
URL url = new URL(baseUrl,"v1/users");
Request request = makeSignedRequestBuilder(certificate, privateKey, url, null, tppSessionId, null)
.get()
.build();
return request;
}
public static Request initiatePayment(X509Certificate certificate, PrivateKey privateKey, URL baseUrl, String debtorAccountBban, String creditorAccountBban, String tppSessionId, String psuId) throws Exception {
/*
Append the API path to the API base url
*/
URL url = new URL(baseUrl,"v1/payments/norwegian-domestic-credit-transfers?allowLogicalDuplicate=true");
/*
Generate sample payment data.
It is important not to have any whitespace in the json structure.
*/
String isoDate = DateTimeFormatter.ISO_DATE.format(LocalDate.now());
String requestBody = "{\"instructedAmount\":{\"currency\":\"NOK\",\"amount\":\"100\"},\"debtorAccount\":{\"bban\":\""+debtorAccountBban+"\",\"currency\":\"NOK\"},\"creditorAccount\":{\"bban\":\""+creditorAccountBban+"\",\"currency\":\"NOK\"},\"creditorName\":\"X\",\"creditorAddress\":{\"country\":\"NO\"},\"purposeCode\":\"COST\",\"requestedExecutionDate\":\""+isoDate+"\",\"remittanceInformationUnstructured\":\"0x5f3759df\"}";
String digest = "SHA-256="+Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256", new BouncyCastleProvider()).digest(requestBody.getBytes(StandardCharsets.UTF_8)));
Request request = makeSignedRequestBuilder(certificate, privateKey, url, digest, tppSessionId, psuId)
.post(RequestBody.create(MediaType.parse("application/json"),requestBody))
.build();
return request;
}
public static Request startAuthorisation(X509Certificate certificate, PrivateKey privateKey, URL baseUrl, String tppSessionId, String psuId, String startAuthorisationLink) throws Exception {
String startAuthorisationLinkNoLeadingSlash = startAuthorisationLink.startsWith("/") ? startAuthorisationLink.substring(1) : startAuthorisationLink;
/*
Append the API path to the API base url
*/
URL url = new URL(baseUrl,startAuthorisationLinkNoLeadingSlash);
Request request = makeSignedRequestBuilder(certificate, privateKey, url, null, tppSessionId, psuId)
.post(RequestBody.create(null, new byte[0]))
.build();
return request;
}
private static Request.Builder makeSignedRequestBuilder(X509Certificate certificate, PrivateKey privateKey, URL url, String digest, String tppSessionId, String psuId) throws Exception {
/*
Request id to identify the request across services. Store this id and its audit trail!
*/
String requestId = UUID.randomUUID().toString();
/*
Base 64 encode the certificate for use in the header
*/
String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded());
/*
Create headers to use for both the signature and the http request
*/
Map<String, String> headers = new LinkedHashMap<String, String>(Map.of("X-Request-ID", requestId,
"Date", ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME),
"TPP-Session-ID", tppSessionId,
"TPP-Redirect-URI", "https://httpbin.org",
"TPP-Signature-Certificate", encodedCertificate,
"ASPSP-ID", "9057",
"PSU-IP-Address", "0.0.0.0" //Use an actual PSU ip address here!
));
if(psuId != null) {
headers.put("PSU-ID", psuId);
}
if(digest != null) {
headers.put("digest", digest);
}
/*
Make all headers lower case.
*/
Map<String, String> normalizedHeaders = headers
.entrySet()
.stream()
.collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue));
/*
Create signature.
*/
String signature = createSignature(certificate, privateKey, normalizedHeaders);
/*
Create a request and set all headers including the Signature header.
*/
Headers.Builder headerBuilder = new Headers.Builder();
normalizedHeaders.forEach(headerBuilder::add);
return new Request.Builder().url(url)
.headers(headerBuilder.build())
.header("Signature", signature);
}
/**
*
* @param certificate Valid X509 certificate registered on a TPP
* @param privateKey The Private key of the x509 certificate
* @param requestHeaders
* @return The signature
* @throws Exception
*/
private static String createSignature(X509Certificate certificate, PrivateKey privateKey, Map<String, String> requestHeaders) throws Exception {
/*
Certificate serial number on upper case hex form.
Example output: 6AEB4444FBAAD267
*/
String upperCaseHEXSerialNumber = certificate.getSerialNumber().toString(16).toUpperCase().trim();
/*
Create the keyId value by Concatenating the serial and issuer name items using SN=***,CA=***
(Note that "CA=" is not part of the issuer name and "SN=" is not part of the serial)
Example output:
SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO
*/
String keyId = String.format("SN=%s,CA=%s",
upperCaseHEXSerialNumber,
certificate.getIssuerX500Principal().getName(X500Principal.RFC2253));
System.out.println("Signature > Key ID: " + keyId);
/*
Filter the headers so only the ones needed for the request are left.
*/
List<String> headersForSigning = requestHeaders.entrySet().stream()
.map(Map.Entry::getKey)
.filter(headerName -> Arrays.asList(signatureHeaders).contains(headerName))
.collect(Collectors.toList());
/*
Create the headers value by concatenating the header names (Concatenate with whitespace *not* comma)
This is used to communicate the header ordering to the ASPSP for signature validation.
*/
String headers = String.join(" ", headersForSigning);
/*
Concatenate all the headers to make the signing string.
They are concatenate in this way:
name1: value1
name2: value2
*/
String signingString = headersForSigning.stream()
.map( headerName -> String.format("%s: %s", headerName, requestHeaders.get(headerName)))
.collect(Collectors.joining("\n"));
/*
Sign the string using RSA SHA256.
The output is a byte array
*/
Signature sha256withRSA = Signature.getInstance("SHA256withRSA", new BouncyCastleProvider());
sha256withRSA.initSign(privateKey);
sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
byte[] signedBytes = sha256withRSA.sign();
/*
Validate signature (this is not strictly neccessary, the purpose in this example is to provide error messages
if there is a problem with the input certificate or key).
*/
/*
Base 64 encode the byte result of the signing algorithm to get a string.
*/
String base64EncodedSignedBytes = Base64.getEncoder().encodeToString(signedBytes);
sha256withRSA.initVerify(certificate.getPublicKey());
sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
boolean valid = sha256withRSA.verify(signedBytes);
if (valid) {
System.out.println(String.format("Signature > VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
} else {
System.out.println(String.format("Signature > NOT VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
throw new SignatureException(String.format("Signed bytes is signed using a private key not matching the certificate with serial %s", upperCaseHEXSerialNumber));
}
/*
Create the Signature header value by concatenating the keyId value, headers value, algorithm used and the base 64 encoded signature.
Example output: keyId="SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO", algorithm="rsa-sha256", headers="date x-request-id tpp-redirect-uri psu-id", signature="***************"
*/
String signature = String.format("keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"%s\",signature=\"%s\"", keyId, headers, base64EncodedSignedBytes);
return signature;
}
private static OkHttpClient createOkHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(60 * 1000, TimeUnit.MILLISECONDS)
.readTimeout(60 * 1000, TimeUnit.MILLISECONDS)
.writeTimeout(60 * 1000, TimeUnit.MILLISECONDS)
.build();
}
public static byte[] readResourceFile(String fileName) throws IOException {
Psd2DEMOClient.class.getResource(fileName);
try (InputStream inputStream = MethodHandles.lookup().lookupClass().getResourceAsStream(fileName)) {
return inputStream.readAllBytes();
}
}
}
Payment Initiation Process
To perform payments the PSU need to be registered with the TPP using SCA once and each payment needs to be signed separately using SCA. (These steps are for a private context, but corporate context is very similar. See Private and Corporate Context)
Register a PSU with the TPP
To register a PSU with the TPP means getting a PSU-ID to use when calling API endpoints on behalf of that PSU. The way a PSU registers with the TPP differs depending on the TPP being only a PISP or also an AISP.
If TPP is only PISP
- Do a GET to /v1/users
- Remember to set the `TPP-Redirect-URI` header (pointing to a resource in your application).
- This will return a 200 OK with data containing a scaRedirect link.
- Redirect the PSU using scaRedirect, or present the link in an iframe to start the SCA flow.
- After completing the SCA flow the PSU will be redirected to the URL provided earlier in the `TPP-Redirect-URI`.
- The redirect will contain a PSU-ID which must be used on consecutive calls to the API.
- Persist the PSU-ID, it should be used in all future requests for this PSU.
Single SCA Payment Flow
In the Single SCA payment flow the TPP is offered a way to initiate a payment without first establishing a PSU-ID for a private customer PSU. To select this flow the TPP must include the header `X-PSU-IDENTITY` set to the personal identification number of the PSU in the payment initiation call (/v1/payments/:product). The personal identity number must be obtained by the TPP directly from the PSU. As part of this process a PSU-ID is created, which can be used by the TPP in subsequent calls if desired. The flow is idempotent, and thus available regardless of whether a PSU-ID has previously been created or not.
If TPP is both PISP and AISP
In this case the PSU gets registered by following the flow in Account Information API Steps.
Perform a Single Payment
After the previous steps you should have a PSU-ID to put in the headers for the rest of the calls.
- Do a POST to /v1/payments/:product
- Refer to the API specifications for details.
- Set the `TPP-Redirect-URI` header (pointing to a resource in your application).
- For this type of call the PSU must always be present, so the header `PSU-IP-Address` is required.
- If the transaction is valid this will return a body containing the transaction and a link to startAuthorization.
- If the transaction incur a fee, this will be indicated by the transactionFeeIndicator property of the response. The relevant fee amount will then be available in the corresponding transactionFees property.
- Follow the startAuthorization link to get a scaRedirect link.
- Redirect the PSU using scaRedirect, or present the link in an iframe to start the SCA flow.
- The PSU will now be presented with the details of the transaction and may choose to sign it using SCA.
- After completing the SCA flow the PSU will be redirected to the url provided earlier in the `TPP-Redirect-URI`.
The transaction status can be checked using the status link from the first POST to /v1/payment/:product.
Payment Initiation API Sequence Diagram
This shows a flow for a PISP who is not an AISP, the part from "InitiatePayment" is the same for both.
Multiple Signatures in Corporate Context
In corporate context some accounts are configured to require multiple signatures for a payment. If the payment status is "PATC" (Partially Accepted Technically Correct), it means that more signatures are required. To complete such a payment the same startAuthorization url must be called for all PSUs which needs to sign a payment.
Example flow where both "PSU A" and "PSU B" needs to sign a payment:
- PSU A initiates and signs a payment just like in a normal payment.
- PISP receives status "PATC" (Partially Accepted Technically Correct) when getting payment status, indicating multiple signatures are required.
- PISP stores the startAuthorisation URL for use by PSU B.
- PSU B indicates that it want to sign the payment.
- PISP calls startAuthorisation again and redirects PSU B to the scaRedirect link from the response.
- The rest of the flow is the same as for a normal payment.
Status Codes
Payments may have status codes updated asynchronously, this is why you should poll for status after the user has approved the payment. Different payment products have different statuses, and SCA exemptions may result in instantly completed payments. See XS2A Framework Implementation Guidelines, Sections 14.12 and 14.13 for details on the payment statuses.
Payments Changed in Other Channels
Prior to November 22nd 2023 the payment references provided to the TPP after payment creation would become stale if payment information was later changed by the customer in a different channel (e.g. internet or mobile bank). On this date a change in the retrieval of payment was implemented so that the original payment reference would remain valid across changes in other channels.
If a payment is changed in a different channel, HATEOAS links must be refreshed by the TPP through a new read of the payment before attempting operations on it. This will provide updated links for the TPP to perform further operations, like payment cancellation.
Status Codes Description
Code | Name | ISO 20022 Definition |
---|---|---|
ACCC | AcceptedSettlementCompleted | Settlement on the creditor's account has been completed. |
ACCP | AcceptedCustomerProfile | Preceding check of technical validation was successful. Customer profile check was also successful. |
ACSC | AcceptedSettlementCompleted | Settlement on the debtor’s account has been completed. Usage: this can be used by the first agent to report to the debtor that the transaction has been completed. |
ACSP | AcceptedSettlementInProcess | All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution. |
ACTC | AcceptedTechnicalValidation | Authentication and syntactical and semantic validation are successful. |
ACWC | AcceptedWithChange | Instruction is accepted but a change will be made, such as date or remittance not sent. |
ACWP | AcceptedWithoutPosting | Payment instruction included in the credit transfer is accepted without being posted to the creditor customer’s account. |
RCVD | Received | Payment initiation has been received by the receiving agent. |
PDNG | Pending | Payment initiation or individual transaction included in the payment initiation is pending. Further checks and status update will be performed. |
RJCT | Rejected | Payment initiation or individual transaction included in the payment initiation has been rejected. |
CANC | Cancelled | Payment initiation has been cancelled before execution. |
ACFC | AcceptedFundsChecked | Preceding check of technical validation and customer profile was successful and an automatic funds check was positive. |
PATC | PartiallyAcceptedTechnicalCorrect | The payment initiation needs multiple authentications, where some but not yet all have been performed. Syntactical and semantic validations are successful. |
PART | PartiallyAccepted | A number of transactions have been accepted, whereas another number of transactions have not yet achieved 'accepted' status. |
Many of these statuses will not be seen in practice because the operations carrying them out happens atomically as seen from the TPP or very quickly.
In addition to these states, payments disappear after they get booked and can then be seen as transactions by AISPs. This happens multiple times daily during the bank's batch settlement operations.
Payment Status Transitions
These are the status transitions you normally will see in practice, and which it makes sense to act upon by a TPP. Note that RJCT is not part of these flows as it can happen at any time before ACSC, and should be handled generally as an error. The same also applies for ACCP with property fundsAvailable=false, which will happen if the debtor account lacks sufficient funds at the time of processing.
Normal Payment
In general payments should be expected to follow this state transition sequence.
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | RCVD | Start authorisation | Yes | Authorize if startAuthorisation link present |
2 | ACSP or PDNG | Poll until next status | Yes (ACSP) / No (PDNG) | PDNG if due date is current date |
3 | ACSC | N/A | No - Payment Complete |
Payment with Instant Completion
Some payments get completed instantly which means polling for status is redundant.
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | RCVD | Start authorisation | Yes | Authorize if startAuthorisation link present |
2 | ACSC | N/A | No - Payment Complete |
Payment with SCA Exemption
SCA exemption results in skipping the RCVD step, note that this can also happen in combination with "Instant Completion".
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | ACSP or PDNG | Poll until next status | Yes (ACSP) / No (PDNG) | PDNG if due date is current date |
2 | ACSC | N/A | No - Payment Complete |
Multiple Signatures
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | RCVD | Start authorisation | Yes | Authorize if startAuthorisation link present |
2 | PATC | Start authorisation again for next PSU | Yes | |
3 | ACSP | Poll until next status | Yes | |
4 | ACSC | N/A | No - Payment Complete |
Periodic Payment
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | RCVD | Start authorisation | Yes | Authorize if startAuthorisation link present |
2 | ACSP | Periodic Payment Registered, operation is complete | Yes |
Cancel Payment After Payment Authorisation
Note that a payment cannot be deleted after it is completed.
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | ACSP | Delete | Yes | |
2 | ACSP | Start authorisation | Yes | Authorize if startAuthorisation link present |
3 | CANC | N/A | No - Deletion Complete |
Cancel Payment Before Payment Authorisation
Step | Code | Next Action | Updatable | Comment |
---|---|---|---|---|
1 | RCVD | Delete | Yes | |
2 | CANC | N/A | No - Deletion Complete |
Payments Clarifications
Deleting Payments
Payments cannot be deleted after they are completed. Whether a payment can be deleted is indicated by the presence of a delete link when getting the payment.
Payments Transition to Transactions
Payments disappear after they are completed and get booked and can then be seen as transactions by AISPs. This happens multiple times daily during the bank's batch settlement operations. Since this is a Payment Initiation API the GET /v1/payment/:id and GET /v1/payment/:id/status resources should be seen as temporary and only meant for tracking the status of payments until they are completed.
Multiple concurrent authorisations
It is possible to have multiple concurrent payment authorisations, this is leveraged by enabling signing baskets described further down. Payment authorisations are also independent of AISP session and consent authorisation, so there is no problem for a TPP to mix requests connected to multiple roles.
Logical Duplicate Error
Two payments can not have the same date, amount, debit account and credit account. This will lead to a Logical Duplicate error. This check can be overridden using the allowLogicalDuplicate query parameter when initiating a payment.
SEPA Credit Transfers
To send payments by SEPA Credit Transfers (SCT) the payment must:
- Be in Euros
- Be sent to a bank in a SEPA country which is a member of the SCT scheme
- Include a valid IBAN for the beneficiary's account
- Include a SWIFTBIC if the payment is to a non-EEA country
The difference between signing baskets and bulk-payments
Signing basket is a vehicle for signing several authorisations at the same time, while bulk-payments is a payment service that lets you transfer from a single debtor account to several creditor accounts in one payment.
Limit of creditors of a bulk-payment
There is no limit of how many creditor accounts you can have in a single bulk-payment.
Bulk-payment payment product support
Bulk-payment does currently only support the norwegian-domestic-credit-transfers product.
Signing Baskets
Signing baskets enable the PSU to sign multiple payments at one time by first creating a signing basket and then perform a single authorisation.
See XS2A Framework Implementation Guidelines, Section 8 and Developer Portal for a detailed description of the signing basket endpoints.
Signing baskets are currently only used for payments, but in the future it is possible that the use will be extended to other resources that requires signatures.
Signing baskets are created with a set of payment ids which have been created using the Payment Initiation API. The authorisation follows the same flow as for a single payment signature. The difference is that it uses the startAuthorisation link in the POST /v1/signing-baskets response instead of the one from the payment response.
Important Notes
- Signing baskets are only a signing vehicle and individual payment authorisations can fail.
- The status of a signing basket is the "most negative" status of all the payments.
- When a signing basket has a status not indicating success, the status of each payment must be queried individually to find detailed error information.
- Signing baskets cannot be deleted, instead create new signing baskets when needed, old signing baskets will be cleaned up after some time.
- Signing baskets are tied to the PSU and cannot be shared. If multiple signatures are needed, create new signing baskets.
Error Codes and Responses
XS2A Framework Implementation Guidelines, Section 14.11 defines the possible error codes. Those lists can be found below where "Message Code" will appear in the "code" field of an error response.
Service Unspecific Error Codes
Message Code | HTTP Response Code | Description |
---|---|---|
CERTIFICATE_INVALID | 401 | The contents of the signature/corporate seal certificate are not matching PSD2 general PSD2 or attribute requirements. |
ROLE_INVALID | 401 | The TPP does not have the correct PSD2 role to access this service. |
CERTIFICATE_EXPIRED | 401 | Signature/corporate seal certificate is expired. |
CERTIFICATE_BLOCKED | 401 | Signature/corporate seal certificate has been blocked by the ASPSP or the related NCA. |
CERTIFICATE_REVOKED | 401 | Signature/corporate seal certificate has been revoked by QSTP. |
CERTIFICATE_MISSING | 401 | Signature/corporate seal certificate was not available in the request but is mandated for the corresponding. |
SIGNATURE_INVALID | 401 | Application layer eIDAS Signature for TPP authentication is not correct. |
SIGNATURE_MISSING | 401 | Application layer eIDAS Signature for TPP authentication is mandated by the ASPSP but is missing. |
FORMAT_ERROR | 400 | Format of certain request fields are not matching the XS2A requirements. An explicit path to the corresponding field might be added in the return message. This applies to headers and body entries. It also applies in cases where these entries are referring to erroneous or not existing data instances, e.g. a malformed IBAN. |
PARAMETER_NOT_CONSISTENT | 400 | Parameters submitted by TPP are not consistent. This applies only for query parameters. |
PARAMETER_NOT_SUPPORTED | 400 | The parameter is not supported by the API provider. This code should only be used for parameters that are described as "optional if supported by API provider." |
PSU_CREDENTIALS_INVALID | 401 | The PSU-ID cannot be matched by the addressed ASPSP or is blocked, or a password resp. OTP was not correct. Additional information might be added. |
SERVICE_INVALID | 400 (if payload) or 405 (if HTTP method) | The addressed service is not valid for the addressed resources or the submitted data. |
SERVICE_BLOCKED | 403 | This service is not reachable for the addressed PSU due to a channel independent blocking by the ASPSP. Additional information might be given by the ASPSP. |
CORPORATE_ID_INVALID | 401 | The PSU-Corporate-ID cannot be matched by the addressed ASPSP. |
CONSENT_UNKNOWN | 403 (if path) or 400 (if payload) | The Consent-ID cannot be matched by the ASPSP relative to the TPP. |
CONSENT_INVALID | 401 | The consent was created by this TPP but is not valid for the addressed service/resource. |
CONSENT_EXPIRED | 401 | The consent was created by this TPP but has expired and needs to be renewed. |
RESOURCE_UNKNOWN | 404 (if account-id in path), 403 (if other resource in path) or 400 (if payload) | The addressed resource is unknown relative to the TPP. An example for a payload reference is creating a signing basket with an unknown resource identification. |
RESOURCE_EXPIRED | 403 (if path) or 400 (if payload) | The addressed resource is associated with the TPP but has expired, not addressable anymore. |
RESOURCE_BLOCKED | 400 | The addressed resource is not addressable by this request, since it is blocked e.g. by a grouping in a signing basket. |
TIMESTAMP_INVALID | 400 | Timestamp not in accepted time period. |
PERIOD_INVALID | 400 | Requested time period out of bound. |
SCA_METHOD_UNKNOWN | 400 | Addressed SCA method in the Authentication Method Select Request is unknown or cannot be matched by the ASPSP with the PSU. |
STATUS_INVALID | 409 | The addressed resource does not allow additional authorisation. |
Payment Initiation Service Specific HTTP Error Codes
Message Code | HTTP Response Code | Description |
---|---|---|
PRODUCT_INVALID | 403 | The addressed payment product is not available for the PSU . |
PRODUCT_UNKNOWN | 404 | The addressed payment product is not supported by the ASPSP. |
PAYMENT_FAILED | 400 | The payment initiation POST request failed during the initial process. Additional information may be provided by the ASPSP. |
KID_MISSING | 400 | The payment initiation has failed due to a missing KID. This is a specific message code for the Norwegian market, where ASPSP can require the payer to transmit the KID. |
KID_INVALID | 400 | The payment initiation has failed due to an invalid KID. This is a specific message code for the Norwegian market, where ASPSP can require the payer to transmit the KID. |
EXECUTION_DATE_INVALID | 400 | The requested execution date is not a valid execution date for the ASPSP. |
CANCELLATION_INVALID | 405 | The addressed payment is not cancellable e.g. due to cut off time passed or legal constraints. |
Account Information Service Specific HTTP Error Codes
Message Code | HTTP Response Code | Description |
---|---|---|
CONSENT_INVALID | 401 | The consent definition is not complete or invalid. In case of being not complete, the bank is not supporting a completion of the consent towards the PSU. Additional information will be provided. |
SESSIONS_NOT_SUPPORTED | 400 | The combined service flag may not be used with this ASPSP. |
ACCESS_EXCEEDED | 429 | The access on the account has been exceeding the consented multiplicity without PSU involvement per day. |
REQUESTED_FORMATS_INVALID | 406 | The requested formats in the Accept header entry are not matching the formats offered by the ASPSP. |
Confirmation of Funds Service (PIIS) Specific HTTP Error Codes
Message Code | HTTP Response Code | Description |
---|---|---|
CARD_INVALID | 400 | Addressed card number is unknown to the ASPSP or not associated to the PSU. |
NO_PIIS_ACTIVATION | 400 | The PSU has not activated the addressed account for the usage of the PIIS associated with the TPP. |
Signing Basket Specific Error Codes
Message Code | HTTP Response Code | Description |
---|---|---|
REFERENCE_MIX_INVALID | 400 | The used combination of referenced objects is not supported in the ASPSPs signing basket function. |
REFERENCE_STATUS_INVALID | 409 | At least one of the references is already fully authorised. |
Sandbox API for Dynamic Mock Data
Sandbox Overview
You can find a sample script for populating the sandbox with mock data here: PSD2 Developer Sample Code
Note: delete operations are cascading which means if you delete a user all agreements and accounts for that user also gets deleted.
The sandbox API is used to populate the sandbox environment with test data.
This makes it easy to test the API with predictable data tailored to the edge cases in your application.
There are mainly two parts to the Sandbox API:
- Managing users and roles
- Managing accounts and transactions
Users and Roles
Users and roles are modelled similarly for corporate and private users. However, the API has been simplified for private users.
To access the data of a customer in both private and corporate the customerNumber is used during the SCA flow.
Private Users
- To create a private user simply perform a POST to /v1/sandbox/customers.
- An agreement is created automatically for you, and accounts added to this customer will automatically be added to that agreement.
- You can see the agreement by calling GET /v1/sandbox/agreements.
Corporate Users
- To create a corporate user first you need to create a user to represent the company.
- Then create one or more accounts with the company as owner.
- Then you need to create one or more corporate users to use as PSUs.
- Then to link the PSUs to the company and accounts you need to POST /v1/sandbox/agreements and include a list of engagements (engagements need the roles "REGISTER" for payment initialisation and "VIEW" for account information).
- To add additional accounts to the user you can call POST /v1/sandbox/agreements/{id}/engagements.
Accounts and Card Accounts
- Accounts and Card Accounts are managed with using POST, GET and DELETE /v1/sandbox/accounts and POST, GET and DELETE /v1/sandbox/card-accounts.
- Accounts and Card Accounts also has transactions which are managed with POST, GET and DELETE /v1/sandbox/accounts/{id}/transactions and POST, GET and DELETE /v1/sandbox/card-accounts/{id}/transactions.
- Accounts with transactions with transaction details are managed with POST, GET and DELETE /v1/sandbox/accounts/{id}/transactions/{transactionId}.
- Account owner should as previously mentioned be the PSU customer in private and a company customer in corporate.
- Accounts are linked to users via agreements and engagements.
Testing Payment functionality
Payment processing in sandbox is handled by a simulated payment processing engine. This engine is configured to behave very much like the processing in production. The processing is scheduled to run 5 times per day, and will include all payments that have the current date as due date. This includes payments created today with today's date as due date, as well as any previously created payments with due date today. The payment engine does not currently support recurring payments.
- Payments can be tested by performing payments to and from mock accounts using the Payment Initiation API
- Payments will show up as transactions on the mock account after the first processing on due date has completed
- Payments will also change the balance of a mock account through the sandbox ledger functionality
- Payments that exceed the available funds on the debtor account will be rejected after 5 batch processing attempts
For details on payment state transitions refer to the section on "Payment Status Transitions".
References
- NextGenPSD2 Framework - Operational Rules, The Berlin Group Joint Initiative on a PSD2 Compliant XS2A Interface, version 1.0, published 08 February 2018
- NextGenPSD2 Framework - Implementation Guidelines, The Berlin Group Joint Initiative on a PSD2 Compliant XS2A Interface, version 1.3 published 19 October 2018
- Signing HTTP Messages https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
- Instance Digest in HTTP https://www.rfc-editor.org/info/rfc3230
Appendix
Javascript Request Signing Code Sample
//signingExample.js
const crypto = require('crypto'); // https://nodejs.org/api/crypto.html
function sign(key, headers) {
var headerStrings = Object.keys(headers).map(function(header) {
return header.toLowerCase() + ": " + headers[header];
}).join('\n');
var signature = crypto.createSign('RSA-SHA256');
signature.update(headerStrings);
return signature.sign(key, 'base64');
}
function format(parameters) {
var supportedParameters = ['keyId', 'algorithm', 'headers', 'signature'];
var parameterStrings = supportedParameters.map(function(param) {
return parameters[param] ? param + '="' + parameters[param] + '"' :
'';
});
return parameterStrings.join(",");
}
createSignature = function(certificate, headers) {
const signingHeaders = ['date', 'digest', 'x-request-id', 'psu-id', 'psucorporate-
id', 'tpp-redirect-uri'];
const headerCopy = {};
Object.keys(headers).forEach(function(key) {
if (signingHeaders.indexOf(key.toLocaleLowerCase()) > -1) {
headerCopy[key] = headers[key];
}
})
try {
const signature = sign(certificate.key, headerCopy);
return format({
keyId: certificate.issuer,
algorithm: certificate.algorithm,
headers: Object.keys(headerCopy).join(' '),
signature: signature
});
} catch (e) {
console.log('failed to create signature', e);
return null;
}
};
createDigest = function(body) {
if (body) {
const digest = crypto.createHash('sha256').update(body,
'utf8').digest('base64');
return 'SHA-256=' + digest;
} else {
return null;
}
}
main = function() {
certificate = { // No blank spaces in the key
// key: content of key.der file that starts with -----BEGIN PRIVATE
KEY----- and end with -----END PRIVATE KEY-----
key=`-----BEGIN PRIVATE KEY-----
private key content
-----END PRIVATE KEY-----
`,
issuer: 'SN=D9ED5432AA92D251,CA=O=PSDNO-FSASOMENAME,
L=Trondheim,C=NO',
algorithm: 'rsa-sha256'
},
htmlBody = "{someJsonContent: 1234}"; // content of body that is sent
in POST requests
headers = {
digest: createDigest(JSON.stringify(htmlBody)),
'x-request-id': '1234567',
'psu-id': 'some-id',
'tpp-redirect-uri': 'https://mydomain.com/callbacksite',
date: 'Fri, 25 Jan 2019 13:42:20 GMT'
}
const signature = createSignature(certificate, headers);
console.log(signature);
}
main();