Payment Initiation Service — Relying Party Developer Guide

1. Overview

The Credit Transfer (Überweisung) enables you to help your end-users to initiate a SEPA credit transfer.

The Credit Transfer product is distinct from regulated payment initiation services where a party initiates a payment on behalf of the user. On a technical level, it is nonetheless described as a Payment Initiation Service, hence called PIS in this specification.

The following sequence diagram depicts the message flow for a payment process with a focus on the messages flowing from you to the OP and back.

We will go through the details of these messages in the following!

qes_flow_puml_rp_view

You need to implement the following steps to let the user initiate a payment with the identity service:

  1. OP (Bank) Selection: First, just as in the identity flow and as explained in the base document, you send the user to the account chooser to select a bank. You will receive an issuer URL from that process. The issuer URL identifies the user’s bank.

  2. Service Configuration Retrieval: You then contact the Service Configuration Service to ensure that the selected issuer is indeed an issuer in the ecosystem and to ensure that the bank’s OAuth Provider (OP) or Authorization Server (AS) behind the issuer supports payment initiation.

  3. Authorization Process: Afterwards, you send the user to the previously selected bank where the user initiates the payment. You will receive an access token from the bank, which can be used to retrieve the payment status (for some banks).

These steps are described in detail in the rest of this document.

1.1. Technologies Used

OAuth 2.0 (grant type authorization code) is the technical foundation of the protocol. It is combined with further OAuth extensions, such as PKCE and OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens, to achieve an appropriate protection level.

1.2. OP (Bank) Selection

The next step, just as in other flows, is to send the user through the OP (Bank) selection at the account chooser. Please refer to the base document for details.

1.3. Service Configuration Retrieval

As a result from the previous step, you will be provided with the Issuer URL of the chosen Bank. You now need to obtain the service configuration from the Service Configuration service.

This part MUST take place in the backend, not in the user’s browser.

To do that, send an HTTP GET request from your backend to the service configuration service URL, attaching the issuer URL in the iss parameter.

For the sandbox, the URL is as follows (here shown with an example issuer): https://api.sandbox.openbanking.verimi.cloud/service-configuration/v1/?iss=https://testidp.sandbox.openbanking.verimi.cloud/issuer/10000001

For production, the URL is the following: https://api.openbanking.verimi.de/service-configuration/v1/?iss={issuer_url}

As with all URL parameters, make sure to properly URL-encode the iss parameter.
Example 1. Service Configuration Request
GET /service-configuration/v1/?iss=https://
   testidp.sandbox.openbanking.verimi.cloud/issuer/10000001 HTTP/2
Host: api.sandbox.openbanking.verimi.cloud
Accept: */*

The API returns an HTTP status code and the service configuration document.

  • If the issuer is found and active, the returned status code is 200 (found).

  • If the issuer is not a valid URI, the returned status code is 400 (bad request).

  • If the issuer is not found, the returned status code is 404 (not found).

  • If the issuer is found but not active, the returned status code is 423 (locked).

If an error is indicated, a JSON document explaining the error is contained in the response body, e.g.: {"error":"invalid_request","error_description":"The issuer 'https://accounts.inactivetestbank.de' is not active"}

When a service configuration document is returned, it is of the following form:

Example 2. Service Configuration with Remote Signature Creation endpoints
{
   "identity": {
      "iss": "https://testidp.sandbox.openbanking.verimi.cloud/issuer/10000001" (1)
   },
   (...)
   "payment_initiation": { } (2)
}
1 This element indicates the OAuth issuer URL that is to be used in the following. Note that this can be different from the issuer URL used so far.
2 The element payment_initiation must be present. If it is not present, the bank does not support payment initiation. You should inform the user about this and give the user the option to select a different bank, by redirecting the user back to the account chooser.

2. Authorization Process

Using OAuth Libraries

Using an existing OAuth library is the best way to implement the following steps. You save a lot of implementation work and get battle-proven, secure code.

Before deciding for a library, we recommend to check for the following features:

  • Does the library support Pushed Authorization Requests?

  • Does the library support RFC8414: OAuth Authorization Server Metadata?

  • Does the library support PKCE?

  • Ideally: Does the library support Rich Authorization Requests?

  • Can the issuer URI be configured during run-time?
    (Some implementations only support one or a number of fixed issuer URIs, while the issuer URI in the identity service is determined dynamically, as described before.)

  • Does the library support TLS Client Authentication using a self-signed certificate at the Token Endpoint as defined in RFC8705?

The list of OpenID-certified client libraries is a good starting point to find suitable implementations.

2.1. Retrieving the OAuth Configuration (Authorization Server Metadata)

First, you (or your OAuth library) need to retrieve the OAuth Configuration, or Authorization Server Metadata document. This document will tell you about the endpoints that you need to use in the following.

The URL of this document is created as follows:

{oauth_issuer_url}/.well-known/oauth-authorization-server

Recall that from here on, only the issuer URL retrieved in the last step is used, not the one returned from the authorization endpoint.
Example 3. OAuth Configuration (Authorization Server Metdata)

The OAuth Configuration document contains, among others, the following keys:

{
   "issuer": "https://testidp.sandbox.openbanking.verimi.cloud/issuer/10000003",
   "pushed_authorization_request_endpoint": "https://testidp.sandbox.openbanking.verimi.cloud/issuer/10000003/par",
   "authorization_endpoint": "https://testidp.sandbox.openbanking.verimi.cloud/services/authz/10000003",
   "token_endpoint": "https://testidp.sandbox.openbanking.verimi.cloud/issuer/10000003/token",
   ...
}
You need to check that the issuer in the configuration document matches the issuer URI which you received from the service configuration!

The pushed authorization request endpoint, authorization endpoint, and token endpoint URLs are used in the following.

2.2. Pushed Authorization Request

The authorization request is sent as a Pushed Authorization Request Pushed Authorization Requests to the authorization server. That means that most of the parameters usually sent in the OAuth authorization request are sent in an MTLS-protected POST request to the backend of the authorization server instead.

This ensures that the data contained in the authorization request remains confidential and cannot be altered by the user or an attacker.

The parameters in the pushed authorization request are shown in the following:

Example 4. Pushed Authorization Request
POST /as/par HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3

response_type=code& (1)
client_id=s6BhdRkqt3 (2)
&state=af0ifjsldkj (3)
&code_challenge_method=S256 (4)
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM(5)
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb (6)
&purpose=payment (7)
&authorization_details=%5B%7B%22type%22%3A%22payment%5Finitiation%
22%2C%22payment%2Dproduct%22%3A%22sepa%2Dcredit%2Dtransfers%22%2C%22
instructedAmount%22%3A%7B%22currency%22%3A%22EUR%22%2C%22amount%22%3
A%22123%2E50%22%7D%2C%22creditorName%22%3A%22Merchant123%22%2C%22cre
ditorAccount%22%3A%7B%22iban%22%3A%22DE02100100109307118603%22%7D%2C
%22remittanceInformationUnstructured%22%3A%22Ref%20Number%20Merchant
%22%7D%5D (8)
1 response_type MUST be code.
2 client_id is your client ID.
3 state is an optional value to carry session state. Note that because PKCE or nonce are used, state is not needed for protection against Cross-Site Reqest Frogery and is therefore optional.
4 code_challenge is the PKCE code challenge. Note the rules in PKCE, Section 4.1. You may additionally use the nonce parameter if this flow is used together with OpenID Connect.
5 code_challenge_method must be S256
6 redirect_uri MUST be one of your pre-registered redirect URIs.
7 purpose is an optional parameter to inform the user about the purpose of the transaction.
8 authorization_details: see below.

This request contains the payment authorization details in the authorization_details parameter.

2.2.1. Authorization Details

The authorization_details parameter is an array of objects as described in Rich Authorization Requests. Each object has a type. The array MUST NOT contain more than one object of type payment_initiation, but may contain other objects, e.g., to combine the payment with a signature, see Advanced Use Cases - Combined Flows.

Example 5. Payment Initiation in authorization_details

The following shows the minimal required data for a payment initiation with the identity service:

[
   {
      "type": "payment_initiation", (1)
      "paymentProduct": "sepa-credit-transfers", (2)
      "instructedAmount": { (3)
         "currency": "EUR",
         "amount": "123.50"
      },
      "creditorName": "Merchant123", (4)
      "creditorAccount": { (5)
         "iban": "DE02100100109307118603"
      },
      "remittanceInformationUnstructured": "Ref Number Merchant" (6)
   }
]
1 type is always payment_initiation (required).
2 paymentProduct is always sepa-credit-transfers (required).
3 instructedAmount is the amount to be transferred, consisting of currency (always EUR) and amount (both required).
4 creditorName is the name of the creditor (required).
5 creditorAccount is the IBAN of the creditor (required).
6 remittanceInformationUnstructured is the reference of the payment (required).
Some banks prevent initiating the same transaction (indicated by the remittanceInformationUnstructured) more than one time per day from the same creditor account. If you intend to send the same transaction for one user multiple times, varying the remittanceInformationUnstructured is required.

You may additionally express a restriction on the debtor account using the debtorAccount JSON object:

Example 6. Restrict to Provided Holder Name
[
   {
      "type": "payment_initiation",
      (... other elements as above ...),
      "debtorAccount": {
         "holderFamilyName": "Doe",
         "holderGivenName": "John"
      }
   }
]

The AS ensures that the name of an account holder of the debtor account matches the provided given name and family name. MUST NOT be used when PIS is used together with IDS or QES in the same flow. Both keys MUST be used together and contain a non-empty string, or omitted.

Example 7. Restrict to IDS/QES Holder Name
[
   {
      "type": "payment_initiation",
      (... other elements as above ...),
      "debtorAccount": {
         "holderSameName": true
      }
   }
]

The key holderSameName with the value true indicates that the same check as above is performed, but the first name and given name are taken from the IDS or QES flow requested at the same time.

MAY only be used when PIS is used together with either IDS or QES. MUST NOT be used in conjunction with an IDS flow unless the given_name and family_name are requested as verified claims within the IDS flow.

Example 8. Restrict to IBAN
[
   {
      "type": "payment_initiation",
      (... other elements as above ...),
      "debtorAccount": {
         "iban": "DE30711860302100100109"
      }
   }
]

The AS ensures that the IBAN of the debtor account matches the provided IBAN. MAY be used standalone or with the holder name restrictions shown above.

2.2.2. Authorization Server Response

The authorization server will respond with a JSON object containing a request_uri parameter representing the newly created pushed authorization request information:

Example 9. Authorization Server Response with request_uri
HTTP/1.1 201 Created
Cache-Control: no-cache, no-store
Content-Type: application/json

{
   "request_uri": "urn:example:bwc4JK-ESC0w8acc191e-Y1LTC2",
   "expires_in": 90
}

The request_uri serves as an identifier that will be used in the next step, the authorization request.

2.2.3. Error Response

In case of an error the response will be structured as described in Pushed Authorization Requests and shown in the following example:

Example 10. Error Response
HTTP/1.x 400 Bad Request

Content-Type: application/json

{
    "error": "invalid_authorization_details",
    "error_description": "the field paymentProduct must not be empty"
}

Possible errors specific to PIS:

HTTP status code

error

error_description (examples)

400

invalid_request

generic error with the request

400

invalid_authorization_details

a problem in the provided authorization details object, e.g., a missing field or a restriction of the debtor account that is incompatible with the selected flow

400

unable_to_enforce_debtor_account

the AS has determined that it cannot enforce the debtor account, e.g., because the IBAN provided is from a different bank

400

access_denied

your client ID is not allowed to use the payment inititation service

2.3. Authorization Request

In the next step, you need to redirect the user’s browser to the authorization endpoint of the bank, passing the request_uri and client_id parameters.

Example 11. Redirection to Authorization Endpoint

Usually, the redirection is done via a Location header and an HTTP status code 302:

HTTP/1.1 302 Found
Location: https://testidp.sandbox.openbanking.verimi.cloud/services/authz/10000003?
      request_uri=urn%3Aexample%3Abwc4JK-ESC0w8acc191e-Y1LTC2&
      client_id=sandbox.yes.com:6a256bca-1e0b-4b0c-84fe-c9f78e0cb4a3`
As always, remember to encode URI parameters properly. Also, the authorization endpoint URL retrieved from the OAuth configuration may already contain parameters that must not be removed. Please make sure that your code or the library used supports parameters in endpoint URIs so that you don’t run into problems because of parameters being cut off or a duplicate ? being used resulting in an invalid URI.

Screen control is with the OP now which will guide your user through login and consent. The OP requires information about your app in the course of this flow. You therefore have to provide a privacy policy and may provide an optional terms of service document and label upon registration.

2.4. Authentication Response

After successful authentication, authorization, and initiation of the payment, the OP will redirect (HTTP status code 302) your user to the {redirect_uri} given in the Authentication Request including the following request parameters:

  • state: if you provided a state value in the pushed authorization request, you MUST check that this value is equal to the state parameter passed to the OP.

  • code: the authorization code you need to get the access token

  • iss: the issuer URL of the OP that created this response. You app MUST check that this value is equal to the issuer URL used in the previous steps. This is a countermeasure against mix-up attacks.

The state value must now be invalidated; i.e., double usage of the same state value MUST result in an error.
The check and the token endpoint request MUST be implemented in the backend logic of the application as this does not expose the results to client side attacks and allows to use MTLS to authenticate the client.
Example 12. Authentication Response
GET /yes/oidccb?code=rDx7qadXAgCrkTGxF7WjrA.gs8gNEgMhH6Ww-VBZbz04w&
        iss=https://testidp.sandbox.openbanking.verimi.cloud/issuer/10000001&state=V1F
        4gV3fDx6y110UzQJpk HTTP/1.1
Host: rpbackend.acme.com

2.5. Authentication Error Response

Errors according to OAuth 2.0 Authorization Error Response and OAuth 2.0 Error Response may occur and must be handled. In case of an error the parameter error will deliver an error code with optional details in the parameter error_description.

The error codes invalid_request, invalid_authorization_details, unable_to_enforce_debtor_account and access_denied as listed above may occur as well.

There is a identity service specific error code account_selection_requested which is returned by the OP if the user clicks 'Select another bank' during the online banking login and shall cause a forced bank selection in the account chooser. You MUST handle this error as described here.

2.6. Token Request

If your app received a successful authentication response, your backend at {redirect_uri} needs to exchange the authorization code for an access token at the token_endpoint . The token request is a POST request with content type application/x-www-form-urlencoded and the following parameters:

  • grant_type: must be set to authorization_code.

  • code: authorization code returned in the authorization response.

  • client_id: your client_id.

  • redirect_uri: same redirect_uri as used in the authorization request. This is a security countermeasure against client impersonation.

  • code_verifier: the PKCE code verifier.

Your RP backend has to perform client authentication with the OP via mutual TLS (aka. TLS client authentication): Your RP has to be configured so that it uses the self-signed certificate with which you registered your RP for the TLS handshake with the OP.
Example 13. Token Request
POST testidp.sandbox.openbanking.verimi.cloud/issuer/10000001/token
Accept: application/json
Accept-encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-length: 261
Host: testidp.sandbox.openbanking.verimi.cloud

grant_type=authorization_code&code=rDx7qadXAgCrkTGxF7WjrA.gs8gNEgMhH6W
w-VBZbz04w&redirect_uri=http%3A%2F%2Frpbackend.acme.com%2Fyes%2Foidccb
&client_id=sandbox.yes.com%3Ae85ff3bc-96f8-4ae7-b6b1-894d8dde9ebe&code
_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

2.7. Token Response

The OP will respond with a JSON object, i.e., the response will contain the header Content-Type: application/json;charset=UTF-8.

The JSON object has the following attributes (and may have others)

  • access_token: the access token

  • token_type: always set to bearer

  • expires_in: seconds until expiration (optional)

  • authorization_details: the authorization details object(s), enriched with important information - see below

Example 14. Token Response
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8

{
   "access_token": "eyJraWQiOi...QIaazQ",
   "authorization_details": [
      {
         "debtorAccount": {
            "iban": "DE30711860302100100109"
         },
         "creditorName": "Test Creditor",
         "creditorAccount": {
            "iban": "DE02100100109307118603"
         },
         "instructedAmount": {
            "amount": "123.50",
            "currency": "EUR"
         },
         "remittanceInformationUnstructured": "This is a test 2T7NRQK20V",
         "type": "payment_initiation",
         "paymentProduct": "sepa-credit-transfers",
         "payment_information": {
            "status_href": "https://pis.example.com/payments/36fc67776/status",
            "txn": "870f3a68-2f4b-47bd-a8b6-df5af6eba432"
         }
      }
   ],
   "token_type": "Bearer",
   "expires_in": 600
}
Note the new element payment_information. It contains a txn identifier that you should store for audit purposes and in case of a dispute. There may be an optional element status_href; since it does not provide additional information with banks currently in the ecosystem, we do not describe it further here.

The successful token response indicates that the payment was received by the bank and first technical checks have been completed.

2.8. Token Error Response

Errors according to OAuth 2.0 Error Response, or OIDC 1.0 Token Error Response may occur and must be handled. In case of an error the returned JSON object will normally contain an error attribute.