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!
You need to implement the following steps to let the user initiate a payment with the identity service:
-
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.
-
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.
-
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.
|
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:
{
"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
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. |
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:
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.
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:
[
{
"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.
[
{
"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.
[
{
"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:
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:
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 |
|
generic error with the request |
400 |
|
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 |
|
the AS has determined that it cannot enforce the debtor account, e.g., because the IBAN provided is from a different bank |
400 |
|
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.
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 astate
value in the pushed authorization request, you MUST check that this value is equal to thestate
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. |
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 toauthorization_code
. -
code
: authorization code returned in the authorization response. -
client_id
: yourclient_id
. -
redirect_uri
: sameredirect_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. |
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 tobearer
-
expires_in
: seconds until expiration (optional) -
authorization_details
: the authorization details object(s), enriched with important information - see below
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.