Compliance APIsBusinessOverview

Developer documentation

The Sandbox offers a safe and simple way to try out Nordea’s APIs. Developers can familiarise themselves with existing and upcoming functionality. Moreover, it allows the developers to experiment and build applications which use the Nordea API before release. See Sandbox for more information regarding the Sandbox and what it is.

The purpose of this documentation is to give an overview of all the APIs which are included in Nordea API Sandbox - both existing production release and upcoming versions. There is separate documentation for each API which includes the full API reference of the particular API.

This API implementation is Representational State Transfer Service (RESTful service in short), and the responses produced by the Nordea API are in JavaScript Object Notation, JSON format. The API also consumes JSON so requests sent to it should have the request body in JSON format.

The links to download the Swagger files can be found here

Note that this API might change in the future based on feedback. Backward compatibility is not guaranteed. This document discusses the latest version of the APIs.

Nordea Business

This section describes the Nordea Business APIs, which allow PSD2-regulated TPPs to access Nordea Business customers’ accounts or initiate payments on their behalf.

‘Nordea Business’ is Nordea’s online banking channel for small to medium enterprise customers.

API overview

Account Information Services (AIS), Payment Initiation Services (PIS) and Access Authorisation APIs are currently supported.

The latest versions of the APIs are as follows:

APIVersion in URL
AISv4
PISv4
Redirect Access Authorisationv5
Decoupled Access Authorisationv5

Account information services

This service provides the ability for PSD2-regulated TPPs with the ‘AISP’ role to request information on one or more Nordea Business customer payment accounts which are accessible online via the Nordea Business banking channel. The information can be categorised as follows:

  • Account list
  • Account details
  • Transaction history

In the Sandbox, developers can create accounts and delete accounts that were created. Also, developers can create transactions for the created accounts. It supports the following request methods: GET, POST, DELETE

Note, that the endpoints related to posting and deleting of the accounts and transactions are provided for the Sandbox only. Please do not put real accounts’ data into the Sandbox. Kindly note that any data uploaded to the system/service may be viewed by Nordea. We do not allow any actual customer data to be stored in our service.

Payment initiation services

This service provides the ability for PSD2-regulated TPPs with the ‘PISP’ role to initiate payments on behalf of a Nordea Business customer from a payment account that is accessible online - and with permission to make payments online - via the Nordea Business banking channel.

Access authorisation

Access Authorisation API is responsible for authenticating the customer (resource owner) and acquiring the customer’s authorisation to share account information with the TPP (client) - and/or authorisation for the TPP to initiate payments on the customer’s behalf. The resulting access token is used by the TPP to consume AIS and PIS APIs.

Terminology

In this section, to avoid confusion, we go through some terminology used in the documentation.

TermExplanation
SandboxSandbox in the context of this API means a mocked service whereby the data returned by the API consists of example data. Its purpose is to mimic the current production and upcoming versions of the API. The Sandbox API will always have the latest version of the API, meaning that all new versions appear in the Sandbox before they are introduced into the production. Developers can create their account and transaction data for use in the Sandbox environment.
Resource ownerThe resource owner refers to the bank customer who uses the client (TPP application) and authorises client access to their accounts
Third Party Provider (TPP)Third-Party Provider (TPP) is the provider of one or more applications which the resource owner (customer) uses. TPP is the client/consumer of the API.
ClientThe client refers to the consumer of the API, which is commonly a TPP application.
API CallAPI call is a request towards the API which receives a response. The API is by design stateless, and therefore it does not “remember” anything about previous requests, i.e., there is no session. Therefore every request made towards the API must contain certain headers so that the API can authenticate and authorise the client.
API ConsoleAPI Console is a tool on the API portal which lets users try out API calls in their web browser quickly.
AuthenticationResource owner (customer) authentication: the process by which the resource owner identifies themselves to Nordea. The client (TPP application) authentication: the process which provides the correct identity of the client; a key component in enforcing that clients can access only the resources that they are allowed to.
Access AuthorisationAccess Authorisation is the process through which the client obtains permission to access the resource owner’s data and services at the bank.
Access ScopesAccess Scopes map to a subset of the consent negotiated between the resource owner (customer) and the client (TPP). Scopes dictate what data the client can access and what services the client can consume.
ConsentConsent is agreed between the resource owner (customer) and the client (TPP). It includes what data is to be shared, what services are to be performed on the resource owner’s behalf, the duration and for what purpose. Once agreed, some of this information is used to start the access authorisation flow through the Access Authorisation API.
Production Version of the APIThe production version of the API refers to the actual release version of the API which this Sandbox mimics. It will allow clients to access the real banking data, subject to the resource owner’s consent being provided to the client.
Access TokenA token which is retrieved by the client after successful access authorisation flow. The access token is passed by the client in all AIS and PIS API calls.
Refresh TokenA token which is retrieved by the client after successful access authorisation flow. The refresh token is passed by the client in token exchange call to obtain a new access token after access token duration has expired.
Authorisation CodeThe authorisation code (or ‘auth code’ for short) is a code provided to the client during the access authorisation flow; this code short-lived and exchanged for an access token.
Availability levelAvailability tells in which part of the delivery lifecycle the given operation, parameter, model or property is.

API HTTP methods

RESTful APIs like this one use HTTP methods to perform actions to fetch, modify, add or delete resources. Here we list methods used by this API.

  • GET - This method reads a resource and returns it. It returns 200 on success.
  • POST - This method creates a new resource. It returns 201 on success.
  • PUT - This method requests that the entity provided is stored in the URI provided. It returns 200 on success.
  • DELETE - This method deletes the resource identified by URI. It returns 200 on success.

Following table shows which methods are supported by the APIs.

APIGETPOSTPUTDELETE
AISXX-X
Redirect Access Authorisation-X--
Decoupled Access AuthorisationXX--
PISXXX-

Note that, for AIS, POST and DELETE are used in the Sandbox only.

API responses and response codes

This API consumes JSON objects and returns responses as JSON objects. Consult the API reference or the examples sections of each API page to see how the response objects look like. The responses are returned encoded in UTF-8 format.

The objects which the API returns as a response are specified in the API reference and they all contain some attributes that are common to all objects. For instance, every object has return code embedded in them which makes it easy for application developers to check whether the request was successful or not. See the examples to learn how to make requests.

Response format

The AIS API response format is following (response data is omitted).

{
  "group_header": {
    "creation_date_time": "2017-06-01T10:12:42Z",
    "http_code": 200,
    "message_identification": "7376382343"
  },
  "response": {
  }
}

Where the group_header contains the http_code, creation time of the response and message identifier. The backend generates the message identifier for each message.

The response contains the actual payload of the response, and the endpoint in question defines its structure.

API response codes

The API response HTTP codes are returned for the user application within the JSON object. These codes can be divided into four categories.

  • 2xx Success
  • 3xx Redirection
  • 4xx Client errors
  • 5xx Server error

The return codes used in this API are covered here or in the API reference.

Connecting to API

To be able to use and connect to the API there are few requirements.

  • Application has to be created.
  • Client ID is required.
  • Client secret is required.
  • Signature is required.
  • Access token is required.
  • Refresh token is required.

The client ID and client secret are generated after the application is created on the My apps page. The client ID and client secret can be found when clicking the app icon after the app is created. The access token and the refresh token must be retrieved by using the Access Authorisation API, see the access authorisation documentation how to do this. The token generation process is also briefly discussed in client authentication section.

When making requests, the client ID, client secret, and access token are sent in a header of each request. Note that both, client ID and secret are always sent, in every single request that is made towards the API.

Here is an example how the parameters look in the request header (some headers are omitted):

Authorization: Bearer YOUR-SECRET-ACCESS-TOKEN-HERE
X-IBM-Client-Id: ababababab-fefe-fafa-b3b3-111112a11111
X-IBM-Client-Secret: saerw4RAWEfwq4ir240jriw40jar2304jra4

If the client secret is lost or leaked, it can be regenerated by clicking the reset button from the apps page. See the examples section of any API to learn how these headers are sent.

Note that the X-IBM-Client-Id and X-IBM-Client-Secret parameters are only used by the client. Also, the access token is made available for the client; the access token should be stored by the client and it should not be given to the end-user.

Client authentication

In the previous section, we described the requirements to connect to the API. The client ID and client secret are parameters which are configured to the client, and they are never exposed to the actual application user.

Initiating the access authorisation flow

See the Access Authorisation API specific documentation for examples how the access authorisation flow works.

Retrieving an access token and a refresh token

See the Access Authorisation API specific documentation for examples how the access authorisation flow works.

Applications

The client (TPP) application might refer to a website, mobile application, and so on. The resource owner (customer) authorises access for the client application (consumer of the API) to use API resources. This authorisation is acquired via the access authorisation flow.

When developing an application, you should also check the security section below.

Migrating application to the production environment

To be able to use the application in the production environment with real data, application owner needs to get access to the production environment. Please raise a support ticket to get more information.

API Swagger definition

The Nordea API specification is also available in the Swagger format. Swagger is the world’s largest framework of API developer tools for the OpenAPI Specification (OAS). More information about it can be found here.

The Swagger files for this API can be downloaded from these links:

In Nordea API there are some extensions in use, as listed below:

  • x-availabilityStatus
  • x-availabilityInfo
  • x-example
  • x-deprecationInfo
  • x-removalInfo

and for monetary amounts, which are decimal strings

  • x-minimum
  • x-exclusiveMinimum
  • x-maximum
  • x-exclusiveMaximum

The x-example is used to specify response examples.

The x-availabilityStatus and x-availabilityInfo are used to specify availability status and additional information about the availability status respectively. The availability status information can be, e.g., an explanation why the given feature has certain availability status. The availability status can be one of the availability statuses listed below (see availability statuses in APIs).

The x-deprecationInfo gives textual information when given feature is deprecated, for example: month, date, quarter or version. X-removalInfo is estimated removal time of the given feature, for example, month, date, quarter or version.

The minimum and maximum extensions are used to specify minimum or maximum amounts for the monetary amounts. The difference between exclusive and non-exclusive is that in the exclusive the minimum or maximum is not included in the value for example, if x-exclusiveMinimum is set to “0” then, “0” cannot be specified as a value and it must be larger than that whereas x-minimum of “0” allows “0” as a minimum value.

Note that to avoid parsing monetary amounts as inaccurate floating point numbers, they are serialized as strings which include the decimal format of the value. These decimal value strings should be parsed by using an appropriate library, for example, BigDecimal in Java or bignumber.js in JavaScript to avoid inaccuracies in calculations. Using floating point numbers can lead to rounding errors and inaccurate results, so please do not use them.

The Swagger specification uses polymorphism to simplify the API definition; it is used in models which share common properties and reduces repetition in models of Nordea API, we use the allOf keyword to inherit base models. More information about the polymorphism can be found here.

As an example, polymorphism is used in the DebitTransaction model which inherits the Transaction model.

Differences between the upcoming production API and Sandbox API

The production version of the API provides access to the real customer data. For example, you will be able to get the real account and transaction information and initiate real payments. The Sandbox is the environment where the application developers can develop their applications before they are promoted to production. The Sandbox API is a superset of the production API, and there will be ‘Sandbox-only’ features in the Sandbox which are never going to be in the production version of the API. The Sandbox and production APIs might have beta features that are subject to change in future because they are developed based on the feedback. These beta features are included so the developers can already test them and give feedback regarding them. The beta features are marked as a beta in the API reference which can be found at the bottom of this page.

Availability statuses in APIs

The availability statuses are meant to inform developers about the feature lifecycle. The availability status is defined for API operations, parameters, models, and properties. These are defined in the API Swagger definition and can also be seen in the API reference.

There are five availability levels, which are:

LevelDescription
BetaThe Beta availability level is for the features that are already implemented but are subject to change in the future based on any feedback. These features can be used by developers in Sandbox and production, but it cannot be guaranteed that breaking changes are not introduced in future. The beta features might be even completely removed in future releases if deemed necessary. Note that also the beta feature changes first appear in the Sandbox environment and then later in the production environment. This way the developers have time to adapt to the upcoming change in the production API.
DeprecatedThe deprecated availability level is for features that will be removed in the future. The actual time for how long the deprecated features will remain supported has yet to be decided but developers will be informed well in advance before features are removed.
DraftThe draft availability level is for features that are still in design phase, and they lack implementation. These are added to give a sneak peek for the developers what is coming to be added to the APIs in the future.
StableThe stable availability level signals that this feature is in production use and should be used in production applications. No breaking changes will be made to the stable features without deprecating them.
Sandbox only”Sandbox only” is an availability level which indicates that the feature is only available in the Sandbox environment and it will never be available in the production environment. For instance, some application testing features have such availability level.

The stable features are found in the Sandbox and production environments. The same is true for the beta and deprecated features.

Draft availability level features are not yet implemented, so they are not found from any environment, they can be only reviewed only from the upcoming API features section.

The beta and draft availability levels are introduced because it will help the API development at the beginning, for example, it is faster to push new features into production. The beta and draft will become less and less important when the API matures.

Feature deprecation and API lifecycle

When a feature is flagged as deprecated, clients will be notified about this. The features are first deprecated in the Sandbox. This acts as an alert that the applications using the API should be updated to adapt to the newer version of the API.

When deprecation is set for a feature, and there will be replacing feature, e.g., if something gets renamed, it will be made available immediately when deprecation is set. The deprecated feature and the replacing feature will co-exist in the API for some period, and after the period, the deprecated feature is removed.

If the feature is removed completely, then it will just disappear after the deprecation period.

Note, that feature here can refer to models, operations, parameters, and properties.

The production version of the API is updated after the Sandbox, that is, the deprecation flags and replacing features first appear in the Sandbox and later to the production API.

API versioning

The API is versioned by the version number in the URL, for example, if the URL is:

https://open.nordea.com/business/v4/some/api/endpoint

Then the version of the API and endpoint in question is version 4 (v4). When new versions are released, the version numbering will be incremented, that means, next version will have the following URL:

https://open.nordea.com/business/v5/some/api/endpoint

Now, when version 5 is released, the old version 4 will still be available and possibly deprecated, meaning that it will still work until removed. When APIs reach deprecated availability status, the application should be updated to use the newer version of the APIs as soon as possible.

The API version number in the URL indicates the major version of the API. There can be minor updates to the APIs which do not change the major version number. The major version changes only when large changes are made which break backward compatibility.

Security considerations

Security is an important aspect, and this API is developed by using well-tested and widely adopted security schemes.

The client ID, client secret, access token and signature are sent in the header of the request. All the communication between the API and client is secured by TLS, which encrypts the traffic. When developing an application, it is important to keep the credentials and tokens safe and handle them carefully to avoid the leakage of the secrets in wrong hands.

There are many good resources online regarding the security, for example Twitters Best Practices: Security guide. Also, OWASPs developer guide is a valuable resource when designing secure web applications.

Error codes and responses

Every response returned by this API has a response code. Response codes can be used to check the result of the requests, for example, was the request successful or did it fail.

The following table shows the return codes used by AIS API:

HTTP Status CodeTextDescription
200OKRequest was fulfilled.
201CreatedThe request has been fulfilled, resulting in the creation of a new resource.
302FoundRedirect.
401UnauthorizedSimilar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided.
403ForbiddenThe request was valid, but the server is refusing action. The user might not have the necessary permissions for a resource.
404Not FoundThe requested resource could not be found but may be available in the future.

Please note that the underlying data model for account information is the same in all AIS responses. Some endpoints however return data for more than one account, that is the list accounts endpoint returns data for multiple accounts whereas the account details endpoint returns data for just one account.

HTTP header fields

Each request requires a set of mandatory HTTP fields, as tabulated below:

FieldDescriptionRemarks
X-Nordea-Originating-DateHTTP header element for date and time represented as RFC 7231 Full Dates. example date: Wed, 24 Apr 2019 14:00:37 EEST, It allows to set it under JavaScript in browser while standard ‘Date’ is forbidden
X-Nordea-Originating-HostHTTP header element for specification of the domain name of the server. It is similar to standard HTTP ‘Host’ header but it won’t be overridden by proxies. It also allows to set it under JavaScript in browser while standard ‘Host’ is forbidden
DigestDigest header as defined in [RFC3230] contains a Hash of the message bodyPSD2 APIs (Access Authorisation, AIS, PIS) from v4 onwards;
SignatureApplication-level signature of the request by the TPP, using QSealCPSD2 APIs (Access Authorisation, AIS, PIS) from v4 onwards;
X-IBM-Client-IdClient ID provided by Nordea, identifying the TPP application
X-IBM-Client-SecretClient secret provided by Nordea’s authentication credentials of the TPP application

As well as the mandatory fields, the following optional fields must be provided when the PSU is present (i.e. in session with the TPP):

FieldDescriptionRemarks
X-Nordea-Originating-User-IpPSU IP address
X-Nordea-Originating-User-AgentPSU user agent

Please note that more information on these mandatory and optional headers is available in the downloadable Swagger definition files and API Reference sections.

Common issues

In this section, we will collect the common issues that the users of the API face. This section will be updated over time.

Access token and code, and refresh token - what are these?

Access token is sent to the API within the request header, it is used by the API to authenticate and authorise the client acting on behalf of the resource owner. Clients cannot use the API without an access token.

Auth code is short-lived, one-time usage and exchanged for an access token when during access authorisation flow.

Refresh token is long-lived, one-time usage and exchanged for an access token after the access token duration has expired.

Problems with access authorisation

Please see the authorisation flow section in Access Authorisation API documentation.

Client secret is lost

If you lose the client secret, you have to generate a new one. To do this, just go to your apps, find the correct app and click its icon and click reset client secret.

Application ID is not registered

If you receive this error when making a request, please double check from the API Market Sandbox that you are subscribed to the APIs you want to use.

API returns 500 and not meaningful error message

In this case, most commonly the request or request payload is invalid. Please check the request you are making carefully. The error messages are not descriptive at the moment, and this issue is noted. The error messages will be improved over time.

Postman collection does not work properly

First, please check that the client secret and client id are set properly.

Next, ensure that Automatically follow redirects is disabled in Postman, for this to work, you need to have the interceptor extension installed and activated for Postman.

Getting started with the API using Postman

In this section, we take a look how to get started with the API by using Postman. If you are new to Postman, see the documentation and tutorials.

First, download the collection for Postman:

v4 & v5:

On how to manage the environments, please see:

Finally, make sure that you disable Automatically follow redirects from the Postman settings. For instructions how to do this, see this.

Code examples and an example application

There are multiple examples on the Github page of Nordea Open Banking. These examples include example application which uses this API, check it out to see how the API can be used programmatically. The readme of the application has instructions how to run it. Note however that the example application SHOULD NOT BE USED IN PRODUCTION. There is, for instance, no TLS configured by default and there are no unit tests, its purpose is just to be an example how the API can be used, so be warned. If you find a bug, have any comments or ideas, please open an issue with the GitHub or make a pull request.

Authentication with eIDAS certificate

Definition of payment service directive and regulatory technical standards

PSD2 and the EBA regulatory technical standards on strong customer authentication and common and secure communication (hereafter the ‘RTS’) regulate the access to payment service users account. RTS (article 34) specifies that for the purpose of identification payment service providers shall rely on QSealC or QWAC certificates.

Nordea will use QSealC -type of eIDAS certificate for TPP authentication as defined by RTS. In practice, this means a digitally signed request where the TPP will use their eIDAS certificate, issued by one of the QTSPs operating in EU/EEA.

A National Competent Authority in each country grants a PSD2 license to the TPP, including a unique ID of the TPP. By acquiring an eIDAS certificate from a QTSP, the certificate holds this unique ID which is then used to identify the TPP in question when establishing a connection to one of the ASPSPs.

For details, see:

Issuers of the eIDAS certificates

There are currently several QTSPs operating in EU/EEA who can issue QSealC certificates. EU maintains a website which lists all issuers. The website is called Trusted List Browser. From the page, a QTSP can be located by selecting a type of service, which should, in this case, be “Qualified certificate for electronic seal” under “Qualified trust services”.

For details, see:

How does it work?

An eIDAS QSealC certificate is used by the TPP to digitally sign all API requests. The signing process is defined by ietf.org in HTTP Message Signatures. The AIS and PIS APIs has a mandatory Signature header. All TPPs are required to use eIDAS certificates for authentication and authorisation after September 14, 2019.

The signature will be sent in the ‘Signature’ HTTP header as described in the RFC. The keyId is the clientId of your application, created during the registration to the API Market.

The request-target is a combination of the HTTP action verb and the request URI path. For example (request-target): get /business/v4/accounts/<accountId> A sample GET and POST request with signature are presented in the following examples.

The following ‘Signature’ headers are required for all GET requests: “(request-target) x-nordea-originating-host x-nordea-originating-date”:

GET: https://open.nordea.com/business/v4/accounts/<accountId> 
X-Nordea-Originating-Host: open.nordea.com
X-Nordea-Originating-Date: Thu, 05 Jun 2019 21:31:40 GMT
Signature:
keyId="<clientId>",algorithm="rsa-sha256",
headers="<signature headers>", signature="<signature code>"

The following ‘Signature’ headers are required for all POST or PUT requests: “(request-target) x-nordea-originating-host x-nordea-originating-date content-type digest”:

POST: https://open.nordea.com/business/v4/payments/domestic
X-Nordea-Originating-Host: open.nordea.com
X-Nordea-Originating-Date: Thu, 05 Jun 2019 21:31:40 GMT
content-type: application/json
digest:
SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
 {
  "amount":
  "2.00",
  "currency":
  "SEK",
  "debtor": {
    "account": {
    "_type": "BBAN_SE",
    "currency":"SEK",
    "value": "5345433453"
               },
  "message": "Test"
           },
  "creditor": {
     "account": {
  "_type": "BBAN_SE",
  "currency":"SEK",
  "value": "434500"
               },
  "message": "Fifth msg",
  "name": "Company A"
            },
  "externalId":"string"
       }
  Signature: keyId="<clientId>",algorithm="rsa-sha256",
  headers="<signature headers>", signature="<signature code>"

Note: Request parameter content-type value depends on the endpoint you are consuming. For example, use application/x-www-form-urlencoded in /token endpoints.

Note: For Sandbox, keyId and algorithm in Signature are static strings clientId and rsa-sha256.

Note: For production APIs, keyId in Signature is the actual client ID provided to the client application and algorithm in signature is a static string rsa-sha256.

Example code: eIDAS signature creation

All of our PSD2 compliance APIs require a signature header created using the request content and signed by eIDAS private key. We follow the HTTP Message Signatures but we have some restrictions as listed below:

  • The only allowed algorithm is rsa-sha256
  • The key size for the used RSA key pair has to be at least 2048 bit
  • The keyId is the clientId of your application originating from the Nordea API Market Sandbox
    • We require the following headers to be used in the signature GET and DELETE request: (request-target) X-Nordea-Originating-Host X-Nordea-Originating-Date
    • POST, PUT and PATCH request: (request-target) X-Nordea-Originating-Host X-Nordea-Originating-Date Content-type Digest
    • The request-target is a combination of the HTTP action verb and the request URI path

The final signature should be created similar to this:

signature="Base64(RSA-SHA256(signing string))"

The following is some sample JAVA code to assist developers in creating the normalised signing string:

import lombok.SneakyThrows;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpRequest;
import org.tomitribe.auth.signatures.Signature;
import org.tomitribe.auth.signatures.Signer;

import java.net.URI;
import java.security.Key;
import java.security.KeyStore;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static java.util.Optional.ofNullable;
import static org.apache.logging.log4j.util.Strings.isNotEmpty;
import static org.springframework.http.HttpMethod.GET;

/**
 * This class is for example purpose only, and is not intended for production use.
 * You must modify it to correspond to your encryption keys and setup.
 * Example of Signature headers:
 * X-Nordea-Originating-Date: Wed, 06 Nov 2019 08:22:42 GMT
 * X-Nordea-Originating-Host: open.nordea.com
 * Signature: keyId="clientId",algorithm="rsa-sha256",headers="(request-target) x-nordea-originating-host x-nordea-originating-date content-type digest",signature="<signature code>"
 * Digest: sha-256=7ehEApNpa/FuZeL9uEVCI/6Mwp41K0bYG71yTS06Kcs=
 */
public class QSealCSignatureExample {

    {%raw%}// Insert keystore passphrase and location according to your environment
    private static final char[] STORE_PASS = "YOUR KEYSTORE PASSPHRASE".toCharArray();
    private static final String keyStoreLocation = "YOUR KEYSTORE.p12";
    // Insert values accordingly to your clientid, host and date
    private static final String keyId = "clientId";
    private static final String originatingHost = "open.nordea.com";
    private static final String originatingDate = "Wed, 06 Nov 2019 08:22:42 GMT";{%endraw%}

    private static final String[] GET_HEADERS = new String[]{"(request-target)", "x-nordea-originating-host", "x-nordea-originating-date"};
    private static final String[] INSERT_HEADERS = new String[]{"(request-target)", "x-nordea-originating-host", "x-nordea-originating-date", "content-type", "digest"};

    private static Key key;

    @SneakyThrows
    public QSealCSignatureExample(@Value(keyStoreLocation) Resource keys) {
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(keys.getInputStream(), STORE_PASS);
        this.key = keyStore.getKey("privatekey", STORE_PASS);
    }

    @SneakyThrows
    public String createSignatureHeader(HttpRequest request, byte[] body) {
        if (request.getMethod() == GET) {
            return createGetSignatureHeader(request);
        }
        return createInsertSignature(request, body);
    }

    @SneakyThrows
    private String createGetSignatureHeader(HttpRequest request) {
        String path = getPath(request.getURI());
        Map<String, String> headers = createMandatoryHeaders();
        Signature signature = new Signature(keyId, "rsa-sha256", null, GET_HEADERS);
        return new Signer(key, signature).sign(request.getMethod().name(), path, headers).toString();
    }

    @SneakyThrows
    private String createInsertSignature(HttpRequest request, byte[] body) {
        String path = getPath(request.getURI());
        Map<String, String> headers = createMandatoryHeaders();
        headers.put("Digest", calculateDigest(body));
        headers.put("Content-type", ofNullable(request.getHeaders().getContentType()).map(Objects::toString).orElse(null));

        Signature signature = new Signature(keyId, "rsa-sha256", null, INSERT_HEADERS);
        return new Signer(key, signature).sign(request.getMethod().name(), path, headers).toString();
    }

    private Map<String, String> createMandatoryHeaders() {
        Map<String, String> headers = new HashMap<>();
        headers.put("x-nordea-originating-host", originatingHost);
        headers.put("x-nordea-originating-date", originatingDate);
        return headers;
    }

    public String calculateDigest(byte[] body) {
        return "SHA-256=" + new String(Base64.getEncoder().encode(DigestUtils.sha256(body)));
    }

    private String getPath(URI requestURI) {
        return requestURI.getRawPath() + getQueryIfNotEmpty(requestURI.getRawQuery());
    }

    private String getQueryIfNotEmpty(String query) {
        return isNotEmpty(query)
                ? "?" + query
                : "";
    }

}

How to test the Sandbox with eIDAS certificate?

For testing TPP authentication with signed requests user of Nordea Sandbox doesn’t need to acquire a real QSealC test certificate. A registered TPP can download a test/demo certificate from Nordea API Market Sandbox and use it for setting up and test API requests. The purposes of this setup are to make the test a simple and quick process, but the common QSealC test certificate obviously is not used for identification in Sandbox. The demo certificate contains all roles (AIS, PIS, etc.)

Before downloading the test/demo certificate from API Market Sandbox, the user must accept the terms and conditions stated beside the download button. The terms and conditions require that the test/demo certificate is used only towards Nordea Sandbox within the EU/EEA area.

Note, that testing with any other certificate than the current one which is downloadable from the API Market Sandbox, will always lead to bad request. You can set signature header to: SKIP_SIGNATURE_VALIDATION_FOR_SANDBOX in case you want to test without a signature.

The downloaded demo certificate is in #p12 format, including the private key and is placed in the .zip container together with some additional files like a readMe.txt -file. The certificate must be extracted from the .zip before the usage. The password for the .p12 certificate file is 1111.

The following flow diagram illustrates the signed API call flow.

eIdas flow diagram

Testing eIDAS signature

Testing eIDAS signature with Postman

Copy script from below and paste it to the Pre-request Script tab in Postman (note that Postman must be a standalone application instead of the Chrome extension):

{%raw%}// Load Forge library 
// !!!!!!!!!!!!!! Unsafe: it is better to download and compile forge lib from https://github.com/digitalbazaar/forge and put it to service managed by you instead of using 
// https://raw.githubusercontent.com/loveiset/RSAForPostman/master/forge.js or just copy whole script to Postman Pre-Script section{%endraw%}
if (!pm.globals.has("forgeJS")) {
    pm.sendRequest("https://raw.githubusercontent.com/loveiset/RSAForPostman/master/forge.js", (err, res) => {
        if(err) {
            console.log(err);
        } else {
            pm.globals.set("forgeJS", res.text());
        }
    });
}
eval(postman.getGlobalVariable("forgeJS"));

{%raw%}// Common{%endraw%}
function getHeaderValue(headerName) {
    const headerValue = request.headers[headerName];
    if (headerValue === undefined) {
        throw new Error(`Requried header: ${headerName} is not defined`);
    }
    return resolveVariables(headerValue);
}

function resolveVariables(textWithPossibleVaraibles) {
    return textWithPossibleVaraibles.replace {% raw %} (/{{(\w*)}}/{% endraw %}g, (str, key) => {
        const value = environment[key];
        return value === null ? "" : value;
    });
}

{%raw%}// Digest Calculation{%endraw%}
function resolveRequestBody() {
    const contentType = getHeaderValue("content-type");
    
    if (contentType === "application/x-www-form-urlencoded") {
        const data = Object.keys(request.data)
            .sort((a, b) => {
                if(a < b) { return -1; }
                if(a > b) { return 1; }
                return 0;
            })
            .map(key => key + "=" + request.data[key])
            .join('&');
        return resolveVariables(data);
    } else if (Object.entries(request.data).length === 0 && request.data.constructor === Object) {
        return "";
    }

    return resolveVariables(request.data.toString());
}

function calculateDigest() {
    const requestData = resolveRequestBody();
    console.log(`Request data: ${requestData}`);

    const sha256digest = CryptoJS.SHA256(requestData);
    const base64sha256 = CryptoJS.enc.Base64.stringify(sha256digest);
    const calculatedDigest = 'sha-256=' + base64sha256;
    
    console.log(`Digest header: ${calculatedDigest}`);
    pm.environment.set("Digest", calculatedDigest);
    return calculatedDigest;
}

// Signature Calculation

const sdk = require("postman-collection");
const moment = require("moment")

const requestWithoutContentHeaders = "(request-target) x-nordea-originating-host x-nordea-originating-date";
const requestWithContentHeaders = "(request-target) x-nordea-originating-host x-nordea-originating-date content-type digest";

function getSignatureBaseOnRequest() {
    const url = new sdk.Url(resolveVariables(request.url));
    const host = url.getHost().toLowerCase();
    const path = url.getPathWithQuery();
    const method = request.method.toLowerCase();
    const date = moment().utc().format("ddd, DD MMM YYYY HH:mm:ss") + " GMT";

    let headers = requestWithoutContentHeaders;

    let normalizedString =
        `(request-target): ${method} ${path}\n` +
        `x-nordea-originating-host: ${host}\n` +
        `x-nordea-originating-date: ${date}`;

    if (method === "post" || method === "put" || method === "patch") {
        const contentType = getHeaderValue("content-type");
        const digest = calculateDigest();
        normalizedString += `\ncontent-type: ${contentType}\ndigest: ${digest}`

        headers = requestWithContentHeaders;
    }
    return {host, path, method, date, headers, normalizedString};
}

function encryptSignature(normalizedSignatureString) {
    const messageDigest = forge.md.sha256.create();
    messageDigest.update(normalizedSignatureString, "utf8");
    return forge.util.encode64(getPrivateKey().sign(messageDigest));
}

function getPrivateKey() {
    let eidasPrivateKey = pm.environment.get("eidasPrivateKey");

    if (!eidasPrivateKey.includes('PRIVATE KEY')) {
        eidasPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n" + eidasPrivateKey + "\n" + "-----END RSA PRIVATE KEY-----";
    }
    console.log(eidasPrivateKey);
    return forge.pki.privateKeyFromPem(eidasPrivateKey);
}

const clientId = getHeaderValue("x-ibm-client-id")
const signature = getSignatureBaseOnRequest();
const encryptedSignature = encryptSignature(signature.normalizedString);
const signatureHeader = `keyId="${clientId}",algorithm="rsa-sha256",headers="${signature.headers}",signature="${encryptedSignature}"`;

console.log(`Normalized signature string: ${signature.normalizedString}`);
console.log(`Signature header: ${signatureHeader}`);

pm.environment.set("Signature", signatureHeader);
pm.environment.set("X-Nordea-Originating-Host", signature.host);
pm.environment.set("X-Nordea-Originating-Date", signature.date);

Add “X-Nordea-Originating-Date” header with value X-Nordea-Originating-Date

Add “X-Nordea-Originating-Host” header with value X-Nordea-Originating-Host

Add “Signature” header with value Signature

In case of POST, PUT or PATCH method you have to also add “Content-Type” and “Digest” header with value Digest

You have to define also eidasPrivateKey env variable. Sandbox private key for static eIDAS certificate is:

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 
Testing eIDAS signature with Java

Sandbox use static certificate for all users: Sandbox Signature

Here is example code which shows how to generate signatures for POST and GET methods with usage of tomitribe-http-signatures library:

Maven:

<dependency>
    <groupId>org.tomitribe</groupId>
    <artifactId>tomitribe-http-signatures</artifactId>
    <version>1.0</version>
</dependency>

Test signature generator:

package com.nordea.openbanking.sandbox.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.tomitribe.auth.signatures.Signature;
import org.tomitribe.auth.signatures.Signer;

import java.security.Key;
import java.security.KeyStore;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

@Component
public class TestSignatureGenerator {

    private static final char[] KEY_STORE_PASSWORD = "1111".toCharArray();
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);

    @Value("classpath:sandbox-signature.p12")
    private Resource sandboxKeyStore;

    public String getPostMethodExampleSignature() throws Exception {
        {%raw%}// Order is important{%endraw%}
        Map<String, String> requestHeaders = new LinkedHashMap<>();
        requestHeaders.put("(request-target)", null);
        requestHeaders.put("X-Nordea-Originating-Host", "api.nordeaopenbanking.com");
        requestHeaders.put("X-Nordea-Originating-Date", DATE_FORMAT.format(new Date()));
        requestHeaders.put("Content-Type", "application/json");
        requestHeaders.put("Digest", "SHA-256=mmbe/mYuzO+LGeL92Nvc4tFu+aSP/NFE8oJoBj8oLOI=");
        return getTestSignatureHeader("POST", "/v4/create", requestHeaders);
    }

    public String getGetMethodExampleSignature() throws Exception {
        {%raw%}// Order is important{%endraw%}
        Map<String, String> requestHeaders = new LinkedHashMap<>();
        requestHeaders.put("(request-target)", null);
        requestHeaders.put("X-Nordea-Originating-Host", "api.nordeaopenbanking.com");
        requestHeaders.put("X-Nordea-Originating-Date", DATE_FORMAT.format(new Date()));
        return getTestSignatureHeader("GET", "/v4/get", requestHeaders);
    }

    private String getTestSignatureHeader(String method, String path, Map<String, String> headers) throws Exception {
        Signature signature = new Signature(
                "clientIdFromHeader",
                "rsa-sha256",
                null,
                headers.keySet().toArray(new String[0])
        );
        Signer signer = new Signer(getTestKey(), signature);
        return signer.sign(method, path, headers).toString();
    }

    private Key getTestKey() throws Exception {
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(
                sandboxKeyStore.getInputStream(),
                KEY_STORE_PASSWORD
        );
        return keyStore.getKey("privatekey", KEY_STORE_PASSWORD);
    }

}

Registration to the API Market

The registration to the API Market is described in the Help Center.

Use of eIDAS qualified seal certificate for third party provider authentication in production environment

After having a license granted by a National Competent Authority, the TPP must purchase an eIDAS QSealC certificate from one of the QTSPs listed in the EU Trusted List. This certificate will be used when enrolling to Nordea Open Banking production. After the successful enrollment, the TPP must use the same certificate in each API call.

Certificate validation and revocation check

When the client certificate is used, it will always be validated by Nordea’s API system. The validation contains normal trust chain validations including expiration check and verification towards revocation on top of the actual signature validation. If any of the validations are not passing the connection will be rejected. Please note that the owner of the certificate must renew the certificate before its expiration. In the case of the new certificate, the enrollment step must be re-initiated.

The role of the TPP mentioned in the certificate is verified and the authorisation must match with the API request in question.

Terminology

WordMeaning
ASPSPAccount Servicing Payment Services Provider. An entity authorised to operate customer accounts, with a line of credit and payment facilities online.
APIApplication Programming Interface. A set of definitions, protocols, and tools that can be used to create applications, interact with other applications, and exchange data.
CertificateAn electronic ‘passport’ used to certify the identity of a person, machine, or organisation over the Internet.
Electronic SealAn electronic ‘signature’ used by a legal entity to certify electronic documents as genuine.
EBAEuropean Banking Authority. The body responsible for publishing the Regulatory Technical Standards (RTS), Implementing Technical Standards (ITS), and a central register for PSD2.
MTLSMutual TLS connection
PSD2Payment Service Directive. EU Directive, administered by the European Commission (Directorate General Internal Market) to regulate payment services and payment service providers throughout the European Union (EU) and European Economic Area (EEA). The Directive’s purpose was to increase pan-European competition and participation in the payments industry also from non-banks, and to provide for a level playing field by harmonizing consumer protection and the rights and obligations for payment providers and users
PSPPayment Service Provider. An entity authorised to provide payment services to customers. PSPs include ASPSPs and TPPs.
QTSPQualified Trust Service Provider. An entity permitted by Member State Supervisory Body to issue Qualified Digital Certificates that are recognized across the EU.
QSealCQualified certificates for electronic seals. Qualified electronic seals can be considered as digital equivalent to seals of legal entities on paper. According to the eIDAS regulation, a qualified electronic seal must be created by a qualified electronic device and based on a qualified certificate for electronic seal.
QWACA qualified website authentication certificate (QWAC certificate) is a qualified digital certificate under the trust services defined in the eIDAS Regulation.
TPPThird Party Provider. An entity authorised to access accounts on behalf of customers but that does not operate those accounts themselves. TPPs include PISPs and AISPs.
RTSRegulatory Technical Standards. Commission Delegated Regulation (EU) 2018/389 of 27 November 2017 supplementing Directive (EU) 2015/2366 of the European Parliament and of the Council with regard to regulatory technical standards for strong customer authentication and common and secure open standards of communication
TLSTransport Layer Security. The TLS protocol aims primarily to provide privacy and data integrity between two or more communicating computer applications
TSPTrust Service Provider. An entity that provides digital services which enable the issuance and proving mechanisms to secure and protect information online. Examples include Certificates and Electronic Seals.