Workflow for OP PSD2 Payment Initiation Service API V2

OP PSD2 Payment Initiation Service API V2 allows Client Applications to create payments on behalf of OP customers, as well as to authenticate the customer and request authorization for submitting the payments for processing.

Go to OP PSD2 PIS API V2 documentation.

This document explains the steps Third-Party Providers need to take in order to access our PSD2 PIS API V2.

All API calls described here are made using mutually authenticated TLS.

Please note that calling PSD2 APIs with Postman is currently not supported.


Contents

References

Terminology & abbreviations

Prerequisites

Statuses of different payment types

Authorization flows

Default authorization flow

Recurring and scheduled payment flow

Exemption flow

Cancellation flow


References

Open Banking Security Profile

FAPI - FAPI Read-Only profile

FAPI-RW

OIDC Connect Core

MTLS


Terminology & abbreviations

  • AISP: Account Information Service Provider
  • ASPSP: Account Servicing Payment Service Provider
  • PISP: Payment Initiation Service Provider
  • PSP: Payment Service Provider
  • PSU: Payment Service User
  • TPP: Third-Party Provider accessing the API

Prerequisites

Before you can start using our PSD2 APIs, there are certain requirements you should meet.

Instructions for accessing the sandbox

Instructions for accessing production environment


Statuses of different payment types

SEPA payment & foreign payment

ActionMandatoryStatus(es)Notes
Create paymentYUnauthorizedPOST request to payment endpoint
Revoke paymentNRevokedOnly an unauthorized payment can be revoked.
Start authorizationYAuthorizingPayment bundle is locked and no new payments can be added to it.
Reject paymentNRejectedThe PSU can reject the payment upon authorization.
Authorize paymentYAuthorized 
PSU authorizes cancellationNCancelledOnly payments with Authorized status can be cancelled by PSU via SCA.
Submit payment: successYAccepted, DebitedAccepted status is most often observed with foreign payment only.
Submit payment: retryable failureNSubmissionPendingIf submission fails, the TPP can try it again after using GET to verify the actual state of the payment. Retries are accepted for the same calendar day.
Submit payment: business failureNRejectedThis indicates payment failure such as insufficient funds.
Payment debited to payer accountN/ADebitedTo get the up-to-date status for a payment, use GET with the paymentId.
Payment has been successfully credited to the payeeN/ACreditedShown only for SEPA INST payments. To get the up-to-date status for a payment, use GET with paymentId.

Recurring payment

ActionMandatoryStatus(es)Notes
Create paymentYUnauthorizedPOST request to recurring-sepa-payments endpoint
Revoke paymentNRevokedOnly an unauthorized payment can be revoked.
Start authorizationYAuthorizingPayment bundle is locked and no new payments can be added to it.
Reject paymentNRejectedThe PSU can reject the payment upon authorization.
Authorize paymentYAuthorized 
Submit payment: successYAccepted 
Submit payment: retryable failureNSubmissionPendingIf submission fails, the TPP can try it again.
Submit payment: business failureNRejectedThis indicates payment failure such as insufficient funds
The PSU authorizes cancellationNCancelledCancellation takes place via SCA flow.
Get paymentNAccepted, AwaitingDueDate, AwaitingFunds, ProcessingPayment status reflects the status of the latest recurrence.
Payment debited to payer accountN/ADebitedThis is the terminal state when all recurrences have been settled. Also possible intermittently, when the latest recurrence has been paid and the next one is yet to be processed.

Payment recurrence states

Individual payment instances within a recurring payment support following states:

ActionStatus(es)Notes
The PSU authorizes cancellationCancelledCancelled by the TPP via SCA or by the PSU via OP's online channels.
Due date has not yet arrivedAwaitingDueDate 
Due date has arrivedProcessingPayment has been locked for background processing.
Due date has arrived, no funds on the payment accountAwaitingFundsBackground processing tries to re-execute the payment for a certain period.
No funds and re-execution period has expiredRejectedBackground processing has stopped unsuccessfully for this recurrence.
Payment debited to payer accountDebitedTo get up-to-date status for a payment recurrence, use GET with paymentID/recurrence/.

Scheduled payment

ActionMandatoryStatus(es)Notes
Create paymentYUnauthorizedPOST request to scheduled-sepa-payments endpoint
Revoke paymentNRevokedOnly an unauthorized payment can be revoked
Start authorizationYAuthorizingPayment bundle is locked and no new payments can be added to it.
Reject paymentNRejectedPSU can reject the payment upon authorization
Authorize paymentYAuthorized 
Submit payment: successYAccepted 
Submit payment: retryable failureNSubmissionPendingIf submission fails, the TPP can try it again after verifying with a GET request the current state of the payment.
Submit payment: business failureNRejectedThis indicates a payment failure such as insufficient funds.
The PSU authorizes cancellationNCancelled 
Get paymentNAccepted, AwaitingDueDate, AwaitingFunds, ProcessingPayment status reflects the status of the latest recurrence
Due date has not yet arrivedN/AAwaitingDueDate 
Due date has arrivedN/AProcessingPayment has been locked for background processing
Due date has arrived, no funds on the payment accountN/AAwaitingFundsBackground processing tries to re-execute the payment for a certain period.
No funds and wait period expiresN/ARejectedBackground processing has stopped unsuccessfully for this recurrence.
Payment debited to payer accountN/ADebitedThis is the terminal state

Authorization flows

OP's PSD2 Payment Initiation Service provides two methods for initiating payments:

Default authorization flow (SCA)

pis-v2-authorization.svg

The default authorization flow implements Strong Customer Authentication (SCA) that can be used in all situations with the PSD2 PIS API. The authorization flow requires completing the following steps:

  1. End user requests to create a payment.
  2. Client App authenticates with Client Credentials Flow.
  3. Client App sets up the payment(s).
  4. End user authenticates and authorizes the payments.
  5. Client App exchanges an authorization code for an access token.
  6. TPP gets the payment(s) (optional).
  7. Client App submits the payment(s).
  8. Client App polls payment status.
  9. TPP verifies the response signature.

Now let's look at each of these steps one by one.

1. End user requests to create a payment

The user simply requests that the TPP client make a payment on their behalf. The client application may receive several payment orders from the end user, committing them at once.

2. Client App authenticates with Client Credentials Flow

To begin interacting with the Payment Initiation Service, the client application authenticates with the Oauth Client Credentials Flow.

The authentication request has the following format:

POST /oauth/token HTTPS/1.1
Host: psd2.mtls.sandbox.apis.op.fi
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=payments&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>

The following cURL call can be used for completing this step. The call assumes that your current directory contains your client certificate (client.crt) and the related private key (key.pem) - both are created by the Registration Helper App.

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/oauth/token -d 'grant_type=client_credentials&scope=payments&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>'

If the request is successful, the response will look like this:

{
  "token_type" : "bearer",
  "access_token" : "Axqx362CnSmLABgzqcBasG0pxBj9",
  "scope" : "payments",
  "status" : "approved",
  "expires_in" : "86399",
}

Save the access_token for use in the next step.

3. Client App sets up the payment(s)

Depending on the payment type, the Client App sets up one or more payments at: - /payments-psd2/v2/sepa-payments - /payments-psd2/v2/foreign-payments - /payments-psd2/v2/scheduled-sepa-payments or - /payments-psd2/v2/recurring-sepa-payments.

Now the TPP application is in possession of an access token, which can be used to create payments. As an additional security measure, the TPP must create a detached JWS and attach it to the payment creation request.

With the default flow, the JWS signature header mandatory.

Creating the detached JWS signature

The TPP uses their QSEALC signing key to create a detached signature of the payment payload. The signature must be set in the x-jws-signature header.

Sample code:

import panva, { JWS, JWKS } from '@panva/jose';

const initPayment = async (
  token: string,
  sessionId: string,
  singlePayment: SepaPaymentRequest
) => {
  const detachedSignature = createDetachedSignature(
    singlePayment,
    ssaSigningKey
  );
  const headers = { 'x-jws-signature': detachedSignature, ...otherHeaders };
  //send payment initiation request with headers:
};

const createDetachedSignature = (payload: any, ssaSigningKey: Buffer) => {
  const jws = asymmetricSign(payload, ssaSigningKey);
  const \[header, b64Payload, signature] = jws.split('.');
  return `${header}..${signature}`;
};

const asymmetricSign = (claims: object, ssaSigningKey: Buffer) => {
  const jwk = panva.JWK.asKey(
    {
      key: ssaSigningKey,
      passphrase: getSecrets().CERT\_PASSPHRASE
    },
    {
      kid: 'example-kid',
      alg: 'RS256'
    }
  );
  const res = panva.JWS.sign(claims, jwk, { kid: jwk.kid });
  return res;
};

Creating a payment

To advance, the TPP establishes mutually authenticated TLS with the host and uses the access token and detached JWS to make a POST call to /payments-psd2/v2/sepa-payments.

cURL:

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/payments-psd2/v2/sepa-payments
-H 'x-api-key: <API_KEY>'
-H 'Authorization: Bearer <ACCESS_TOKEN>'
-H 'x-jws-signature: eyJraWQiOiJxZXNlYWwtQWZVeHhnIiwiYWxnIjoiRVMyNTYifQ..EHALGjFFfu2URCHuE4ZWPIrvNkkINgAiid_4370_a0GAyXz2pRa9Umsam_HoHKEBrdhOSz9dJ9QWLpq-Y7zC0w'
-H 'x-fapi-interaction-id: 351cd754-1177-4f87-812e-3a996a44aba8'
-H 'Accept: application/json'
-H 'Content-Type: application/json'
-H 'x-idempotency-key: 10b2a89b-c491-4377-8fe0-a72d52028f3f'
-d '{"payee":{"iban":"FI2112345600000785","name":"test"},"amountEUR":"1.50","message":"test","payer":{"iban":"FI1258410220189833"}}'

Response body:

{
  authorizationId: '96b5076c-e2cc-41e0-8521-073e35f56731',
  paymentId: 'b4fac615-0fd7-441a-bbe3-271e1221aea2',
  created: '2022-12-21T17:48:13.424+02:00',
  modified: '2022-12-21T17:48:13.424+02:00',
  payer: { iban: 'FI1258410220189833' },
  message: 'test',
  amountEUR: '1.50',
  payee: { iban: 'FI2112345600000785', name: 'test' },
  status: 'Unauthorized'
}

After creating the first payment, the client application receives an authorizationId. If multiple payments are to be authorized in one bundle, subsequent POST requests to /payments-psd2/v2/sepa-payments can include this authorizationId in the request body. The table below explains the relationship between the authorizationId and paymentId.

NameLegend
authorizationIdA value representing a single bundle of payments. Can be reused if several payments are to be authorized simultaneously.
paymentIdA string representing and identifying the individual payment resource. Used when submitting authorized payments.

With the authorization ID, the client application can create a bundle of payments by replaying the value when creating additional payments. Payments are created one-by-one, but the authorization ID links them together.

Examine or delete created payments

After creation, a single payment can be examined or deleted (i.e. erased before authorization) with a GET or DELETE call to /payments-psd2/v2/sepa-payments/{paymentId}.

Examine and revoke authorizations

The authorization ID can be used for examining the details of the authorization and/or the payments associated with it, or for deleting it, revoking the entire bundle of associated payments.

4. End user authenticates and authorizes the payments.

Authentication and authorization take place between the end user and the authorization server of the account service. The role of the TPP is to prepare an appropriate JWT request, fill in the necessary query parameters, and redirect to the authorization address.

4.1 Constructing the authorization request

The request is a JSON Web token that has been signed with a QSEALC private key (or a key which emulates QSEALC) using RS256 or ES256. The corresponding public key must be made available at a registered JWKS endpoint (not required in sandbox).

Padding characters ("=") should be omitted as per https://tools.ietf.org/html/rfc7515#section-2.

The header and payload of the JWT request have the following structure:

{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "Vi3hb" --> This value varies per key
}
.
{
  "aud": "https://mtls.apis.op.fi",
  "iss": "1ztgnp8NZzUmlmiAdNoC",
  "response_type": "code id_token",
  "client_id": "1ztgnp8NZzUmlmiAdNoC",
  "redirect_uri": "https://localhost:8181/oauth/access_token",
  "scope": "openid payments",
  "state": "bb16b497-097f-4528-9380-4305eb760232",
  "nonce": "76fb7d3a-3a14-4bf7-96fd-e770c3c1e33e",
  "max_age": 86400,
  "exp": 1613548257,
  "iat": 1613547357,
  "claims": {
    "userinfo": {
      "authorizationId": {
        "value": "976da7b0-1db2-410d-a5c9-e3a37bc4d7a1",
        "essential": true
      }
    },
    "id_token": {
      "authorizationId": {
        "value": "976da7b0-1db2-410d-a5c9-e3a37bc4d7a1",
        "essential": true
      },
      "acr": {
        "essential": true,
        "values": [
          "urn:openbanking:psd2:sca"
        ]
      }
    }
  }
}
.
<signature>
FieldTypeRequiredLegend    
typstringtrueType of the token. Always gets value JWT.    
algstringtrueThe signing algorithm used. Possible values are RS256 and ES256.    
kidstringtrueID of the signing key. A key corresponding to this value must be available at a public JWKS endpoint. If you have used the registration app in sandbox, this value is found in the field ssaSigningKid in registration-result.txt (the key is in ssa-signing-key.pem).    
audstringtrueIntended audience of the JWT, i.e. the host URL of the API.    
issstringtrueIssuer of the token, i.e. Client ID of the TPP.    
response_typestringtrueSpecifies the type of the authorization flow. Allowed values for the default authorization flow are code and code id_token. When using the exemption flow, the value must be token.    
client_idstringtrueUnique identifier of the client application.    
redirect_uristringDefault flow: true, Exemption flow: falseThe address to which the end user will be redirected after authentication and authorization. The access token or error descriptors will be attached to this uri as query parameters.    
scopestringtrueScope names that identify the extent of the requested authorization, as a list of space-separated names. Per the OIDC specification all requests (exemption flow notwithstanding) must contain the openid scope. Only one of the following scopes is allowed for each request: accounts, payments, fundsconfirmations. For PIS default flow, the required value is openid payments. In the exemption flow, the value must be payments.    
statestringtrueA string that will be returned to the TPP. Used for validating responses and correlating requests with responses. Must only contain supported characters specified in RFC 4648 Section 5, Table 2.    
noncestringtrueA unique string that is used for protecting against replay attacks. Generated by the Client App.    
max_agenumbertrueMaximum allowed age for the authentication, in seconds.expnumbertrueToken expiry timestamp
iatnumbertrueToken issued at timestamp    
claimsobjecttrueContains information related to the request. The field authorizationId contains the authorizationId obtained while registering intent.    
userinfoobjecttrueContains information related to the request. The field authorizationId contains the authorizationId obtained while registering intent.    
id_tokenobjecttrueThe contents of this field are similar to userinfo.    
acrobjtrueSpecifies the requested authorization contexts. In the default flow, the value must be urn:openbanking:psd2:sca. In the exemption flow, the value must be urn:openbanking:psd2:ca    

Once the body and header have been constructed, follow these steps:

  1. Base64 URL encode the header and body separately. Append the body to the header, separating the two parts with a dot.
  2. Create a signature by taking a copy of the newly created string and encrypting it with your private key (from QSEALC or emulated) using the algorithm specified in the header. This is the signature.
  3. Attach the signature to the encoded header and body with a dot. You'll get something like this:
eyJhbGciOiJIUzI1NiJ9.eyJtc2ciOiJIZWxsbyJ9.N6-M8oOwCq9x820PR6HMtWo-_FL5GeWUSqQ66H2F5kA

Remember to leave out padding characters ("=").

See our example code for signing JWTs. This code assumes that you have private and public JWKS readily available. In the sandbox, developers may use the Registration Helper App to auto-generate JWKSs.

ES256 or RS256 are allowed as signing algorithms.

import * as fs from "fs";
import { JWKS, JWK, JWT } from "jose";

//request is a valid pre-created JWT body
const asymmetricSign = (request: object, privateJwksPath: string) => {
  const privateJwks = JSON.parse(
    fs.readFileSync(privateJwksPath, "utf8").toString()
  );
  const privateJwk = JWKS.asKeyStore(privateJwks, {
    calculateMissingRSAPrimes: true,
  }).get({ alg: "RS256" });
  return JWT.sign(JSON.stringify(request), privateJwk);
};

You can validate the generated JWT with your public JWKS as follows:

import * as fs from 'fs';
import { JWT } from 'jose';

const publicJwks = JWKS.asKeyStore(JSON.parse(fs.readFileSync('path/to/publicjwks.json', 'utf8').toString()));

JWT.verify(jwt, publicJwks, {
    algorithms: \['ES256', 'RS256', 'PS256'],
    clockTolerance: '120s',
    maxTokenAge: '60s',
    complete: true,
});

Learn more about JWTs on jwt.io. Note that the JWT editor is currently incapable of processing this type of key.

4.2 Redirection

Once the request is complete, the TPP will redirect the customer to OP's auth server. The client app must append the following query parameters to the redirection URI:

  • request (the JWT request)
  • response_type
  • client_id
  • scope

The values of the parameters MUST match with those in the JWT request. Remember that all query parameters must be URL encoded.

The redirect will have the following structure:

https://authorize.psd2-sandbox.op.fi/oauth/authorize
?request=<your_JWT_string>
&response_type=code+id_token
&client_id=******
&scope=openid%20payments

Direct the customer to this URI in the browser.

4.3 The customer authenticates and authorizes

Authentication and authorization are the responsibility of the customer and the authorization server. The customer logs in and selects the authorizations they wish to confirm or reject.

The following test credentials are available in our sandbox:

UsernamePasswordKey code / mobile key
8888888111221122
8888888211221122

4.4 Back to Client Application

The auth server will redirect the end user to the redirect URI specified in the request. There are two possible outcomes:

Success:
<REDIRECT_URI>?state=<state>&code=<authorization_code>

Success with ID token:
<REDIRECT_URI>#state=<state>&code=<authorization_code>&id_token=<id_token>

Error:
<REDIRECT_URI>?state=<state>&error=<error_description>

For example:
https://localhost:8181/oauth/access_token?error=temporarily_unavailable&state=373b23cd-a1b3-449a-810f-cd4b36f7a60c

As the user returns to the TPP's application, the TPP should verify that the returning user is the same as the user who began started the authorization process. This can be achieved by inspecting the user's session ID.

ID tokens are created and returned on two occasions:

  1. When using response type code id_token in the authorization request. The ID token will be delivered in a query parameter attached to the client app's redirect URI. Note that when an ID token is present, the query string is in fact an URI fragment (begins with "#" instead of "?").
  2. When requesting a token from a token endpoint and scope includes openid (in OP PSD2, always). The token will be returned in the response body to the token request.

The ID token contains information about the authorization, and provides an additional layer of security.

The ID token contains the following information:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "12345"
}
.
{
  "iss": "https://authorize.psd2-sandbox.op.fi",
  "iat": 1234569795,
  "sub": "28c52f98-7541-42a2-9ca0-b86ceac47692",
  "acr": "urn:openbanking:psd2:sca",
  "authorizationId": "28c52f98-7541-42a2-9ca0-b86ceac47692,
  "aud": "s6BhdRkqt3",
  "nonce": "n-0S6_WzA2Mj",
  "exp": 1311281970,
  --ALTERNATIVE 1: AUTHORIZATION CODE FLOW---
  "s_hash": "76sa5dd",
  "c_hash": "asd097d"
  --ALTERNATIVE 2: TOKEN ENDPOINT---
  "rt_hash": "wFsWWP49hHo4sXwwVSHX-A",
  "at_hash": "bSUTLJiqrooANhJJ2X7pPw"
  }
.
{
<<signature>>
}
FieldTypeLegend
issstring/URIIssuer of the token.
iattimestampToken issuance time as seconds since 1970-01-01T0:0:0Z.
substringThe authorization ID to which the ID token relates to.
acrstringAuthentication Context Class Reference, i.e. the level of authentication achieved. Currently, only urn:openbanking:psd2:sca is supported.
authorizationIdstringThe authorization ID to which the ID token relates to.
audstring/client IDIntended audience of the ID token. WIll contain the Client ID of the Client Application.
noncestringA value originally provided by the TPP in the request, used for protecting against replay attacks.
exptimestampTime of expiry, i.e. the time after which the token is no longer valid.
s_hashstring, case-sensitiveReturned with authorization code. State hash value: base64Url encoding of the left-most half of the hash of the octets of the ASCII representation of the state value, where the hash algorithm used is the hash algorithm used in the algHeader Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the code value with SHA-256, then take the left-most 256 bits and base64url encode them. The s_hashvalue is a case sensitive string.
c_hashstring, case-sensitiveReturned with authorization code. Code hash value: base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header.
rt_hashstringReturned from token endpoint. Refresh token hash value: base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the refresh_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the refresh_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The rt_hash value is a case sensitive string.
at_hashstringReturned from token endpoint. Access token hash value: base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case sensitive string.

The ID token must be validated following the OIDC spec. In addition, the TPP should do the following:

Alternative 1: ID token received in redirection URI

  • Validate the authorization ID against that received earlier.
  • Validate the signature of the ID token.
  • Validate s_hash (state hash).
  • Validate c_hash (code hash).

Alternative 2: ID token in access token response body

  • Validate the authorization ID against that received earlier.
  • Validate the signature of the ID token.
  • Validate rt_hash (refresh token validation).
  • Validate at_hash (access token validation).

See the public key for validating ID token signatures.

5. Client App exchanges an authorization code for an access token

At this point the TPP is in possession of an authorization code. This must now be exchanged for an access token.

POST /oauth/token HTTPS/1.1
Host: psd2.mtls.sandbox.apis.op.fi
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<redirect_uri>&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>

cURL:

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/oauth/token -d 'grant_type=authorization_code&client_id=<clienti_id>&client_secret=<APP_CLIENT_SECRET>&code=wpj94iuZRzFvmHHpIFba7b1QiZweXm&redirect_uri=https%3A%2F%2Flocalhost%3A8181'

A successful request will yield a response with the following content:

{
  "access_token":"TfOJ0yOywdg8cwthvKUE",
  "refresh_token":"TYiOBpLHMWSgXsTg0SJ7TlqVMhSsWnJ6vxL6dUk9",
  "id_token": "eyJraWQiOiI2ZzNRRU9SY0hNOVdVZTZlcVowZi1wdjNEUEV1Y2NHVTRUdDFVMHo0cFIwIiwiYWxnIjoiUlMyNTYifQ.eyJydF9oYXNoIj---90ZXN0X2NsaWVudCJ9.OMxF_fyVTS0af_---e9sQXEGMHW_S7bsA",
  "scope":"openid payments",
  "token_type":"bearer",
  "expires_in":3599,
}
FieldtypeLegend
access_tokenstringThis is the access token. Use as a bearer token in the Authorization header to access protected resources.
refresh_tokenstringCan be used to request additional access tokens. Valid for up to 45 days (Less if the authorization granted is shorter).
id_tokenstringA signed JWT containing information about the authorization. See ID tokens for a full description on the contents and recommended validation procedures for ID tokens.
scopestringA space-separated list of scope names describing the services to which the token grants authorization.
token_typestringToken type. Always has the value "bearer".
expires_innumberThe period for which the token is valid, in seconds.

Refreshing tokens

Refresh tokens may be used for requesting additional access tokens, as well as for renewing refresh tokens. They remain valid for a maximum of 45 days.

Regardless of the length of the authorization, the end user must re-authorize the Client Application if the application fails to renew the refresh token within 45 days.

Refresh requests have the following structure:

POST /oauth/token HTTPS/1.1
Host: psd2.mtls.sandbox.apis.op.fi
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<refresh_token>&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>

The response to this request is similar to that of a normal token request.

6. TPP gets the payment(s) (optional)

TPP can GET the payments here to retrieve the payer account and account holder name, for example.

7. Client App submits the payment(s)

Armed with the access token and payment IDs, the Client Application submits each individual payment with a POST call to

/payments-psd2/v2/<payment type>/{paymentId}/submit.

Payment must be submitted immediately after authorization. For future dated payments use scheduled payments.

8. Client App polls payment status

TPP invokes the /payments endpoint for querying payment status. The status of each payment can be polled using paymentId.

9. TPP verifies the response signature

NOTE! Response signature verification applies to any response sent by PSD2 PIS API.

As a final verification step, the TPP must inspect the detached JWS signature of the body of the payload of the header x-jws-signature.

The format of the signature is header..signature, where header is a JWS header and signature a JWS signature of a usual compact serialized JWS.

The payload received in the response body must be inserted into the middle of the JWS string in as shown:

header. + BASE64URL(received payload) + .signature

Read further details.

To finish, the TPP verifies the resulting compact serialized JWS (header.payload.signature) against a matching JWK.


Recurring and scheduled payment flow

As of PIS API V2, recurring and scheduled payments are automatically submitted by a background process. The payments can be modified by the PSU in OP's online channels and the TPP can cancel the payment via SCA (see Cancellation flow).

pis-v2-scheduled-recurring.svg

Exemption flow

The exemption flow allows creating and submitting payments by reusing a previous authorization. When using the exemption flow, the TPP does not need to direct the customer to OP's authentication and authorization UI. The exemption flow is available when the payment adheres to the conditions and requirements described in this section.

The exemption flow is available both in sandbox and production.

Note that the exemption flow can only be used with immediate SEPA payments.

Conditions and requirements

The following conditions must be met:

  • The user has authorized the TPP app before (client possesses refresh token from an earlier SCA).
  • The payer account is owned by the person whose authorization is being reused.

Furthermore, the payment must fall under one of these categories:

  • Payment amount is less than 30 euros, the user has submitted fewer than 5 exemption payments since last SCA, and the cumulative value of the customer's exemption payments since latest SCA is less than 100 euros.
  • The payment is a transfer between two OP accounts owned by the customer.

Additionally, the following limitations apply:

  • Only SEPA payments are allowed.
  • Payments cannot be bundled together with a single authorization ID.

If the exemption flow fails at any point, the payment must be completed using the default authorization flow.

Overview of the exemption flow

pis-v2-exemption.svg
  1. Customer requests to create a payment.
  2. Client App uses a refresh token to request an access token for the customer.
  3. Client App creates the payment.
  4. Client App creates a request JWT, confirming the reuse of the previous authorization.
  5. Client App submits the payments one by one.

1. Customer requests to create a payment

As with the default flow, the process begins with the customer requesting to commit a payment. The payment must be within the bounds of the exemption flow. See conditions and requirements.

2. Client App uses a refresh token to request an access token for the customer

The Client App requests an access token from the token endpoint. This replaces Client Credentials Grant of the default authorization flow. The client uses the refresh token from a previously completed SCA instead of an access token representing the client itself:

POST /oauth/token HTTPS/1.1
Host: psd2.mtls.sandbox.apis.op.fi
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<REFRESH_TOKEN_FROM_SCA>&scope=payments&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>

cURL:

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/oauth/token -d 'grant_type=refresh_token&refresh_token=<REFRESH_TOKEN_FROM_SCA>&scope=payments&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>'

3. Client App creates the payment

The client creates the payments as with the default flow. Note that the exemption flow does not allow payment bundling: each payment must be created and authorized separately.

When using the exemption flow, the header x-jws-signature is mandatory. See step 3 of the default flow for more information on how to construct the header contents.

The headers x-fapi-customer-ip-address, and x-customer-user-agent and x-fapi-customer-last-logged-time should also be set.

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/payments-psd2/v2/sepa-payments
-H 'x-api-key: <APP_API_KEY>'
-H 'Authorization: Bearer <ACCESS_TOKEN>'
-H 'x-jws-signature: eyJraWQiOiJxZXNlYWwtQWZVeHhnIiwiYWxnIjoiRVMyNTYifQ..EHALGjFFfu2URCHuE4ZWPIrvNkkINgAiid_4370_a0GAyXz2pRa9Umsam_HoHKEBrdhOSz9dJ9QWLpq-Y7zC0w'
-H 'x-fapi-customer-ip-address: <IP_ADDRESS>'
-H 'x-customer-user-agent: <USER_AGENT>'
-H 'x-fapi-customer-last-logged-time: Sun, 10 Sep 2017 19:43:31 UTC'
-H 'x-fapi-financial-id: test'
-H 'Accept: application/json'
-H 'Content-Type: application/json'
-H 'x-idempotency-key: idempotent_5'
-d '{"payee":{"iban":"FI4950009420028730", "name":"Test_Person"}, "payer":{"iban":"FI5850009420172322"}, "message":"test_payment", "amountEUR":"5.00"}'

4. Client App creates a request JWT, confirming the reuse of the previous authorization

The Client constructs a JWT request as in the default authorization flow, with minor differences in its structure. Below is a sample exemption JWT body:

{...}
.
{
  "iss": "<CLIENT_ID>",
  "aud": "https://mtls.apis.op.fi",
  "response_type": "token",
  "client_id": "<CLIENT_ID>",
  "scope": "payments",
  "state": "1234",
  "nonce": "ttpp1573632173204",
  "max_age": 86400,
  "claims": {
    "id_token": {
      "acr": {
        "values": [
          "urn:openbanking:psd2:ca"
        ],
        "essential": true
      },
      "authorizationId": {
        "value": "26c71e03-e5e9-43b9-b913-36f30baf26d2",
        "essential": true
      }
    },
    "userinfo": {
      "authorizationId": {
        "value": "26c71e03-e5e9-43b9-b913-36f30baf26d2",
        "essential": true
      }
    }
  }
}

Differences to default JWT requests

FieldDifference
response_typeMust be token.
redirect_uriNot required with exemption flow.
scopeMust be payments.
acrThe array field values must only contain value urn:openbanking:psd2:ca.

Once the exemption JWT is ready, the Client App uses it to fetch a new access token. The JWT request must be transmitted to the token endpoint in the body parameter assertion, along with the Client App's client credentials. The scope is payments.

Body parameters of the token call

NameDescription
client_secretThe Client Secret of the application.
grant_typeGrant type. Must be urn:ietf:params:oauth:grant-type:jwt-bearer.
assertionThe JWT request constructed for the exemption flow.
response_typeOauth response type. Must be token.

Example call to the token endpoint with JWT assertion

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/oauth/token -d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<EXEMPTION_JWT>&response_type=token&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>&scope=payments'

5. Client App submits the payments one by one

As with the default authorization flow, the Client App finally submits the payment using the newly acquired access token and receives its status in the JSON response.

cURL:

curl -vk --key key.pem --cert client.crt -X POST https://psd2.mtls.sandbox.apis.op.fi/payments-psd2/v2/sepa-payments/0427a7ba-2c83-4556-9fc9-98eee25c34ee/submit -H 'x-api-key: <API_KEY>' -H 'Authorization: Bearer <ACCESS_TOKEN>' -H 'Content-Length: 0'

Cancellation flow

Cancellation of a payment requires separate SCA. SEPA and foreign payments can only be cancelled when their status is Authorized.

Recurring and scheduled payments can be cancelled before the last due date.

pis-v2-cancellation.svg

The cancellation flow implements Strong Customer Authentication (SCA). The authorization flow requires completing the following steps:

  1. The End user authenticates and authorizes the cancellation.
  2. The Client App exchanges an authorization code for an access token.
  3. The Client App cancels the payment(s).
  4. The Client App polls payment status.
  5. TPP verifies the response signature.

Now let's look at each of these steps one by one.

1. End user authenticates and authorizes the cancellation

Authentication and authorization take place between the end user and the authorization server of the account service. The role of the TPP is to prepare an appropriate JWT request, fill in the necessary query parameters, and redirect to the authorization address.

1.1 Constructing the authorization request

The request is a JSON Web token that has been signed with a QSEALC private key (or a key which emulates QSEALC) using RS256 or ES256. The corresponding public key must be made available at a registered JWKS endpoint (not required in sandbox).

Padding characters ("=") should be omitted as per https://tools.ietf.org/html/rfc7515#section-2.

The header and payload of the JWT request have the following structure:

{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "Vi3hb" --> This value varies per key
}
.
{
  "aud": "https://mtls.apis.op.fi",
  "iss": "clientId",
  "response_type": "code id_token",
  "client_id": "clientId",
  "redirect_uri": "https://tpp-demo-app.psd2-sandbox.test.aws.op-palvelut.net/oauth/access_token/payments",
  "scope": "openid payments:delete",
  "state": "80c00b913696c4a7c9598470",
  "nonce": "94052e00990cc84c6accbab4",
  "max_age": 86400,
  "exp": 1675755458,
  "iat": 1675754558,
  "claims": {
    "userinfo": {
      "authorizationId": {
        "value": "bfff2142-25dc-4c50-a4c5-000eb9f652c5",
        "essential": true
      },
      "paymentId": {
        "value": "371065fa-f134-4a43-815a-efa741407541",
        "essential": true
      }
    },
    "id_token": {
      "authorizationId": {
        "value": "bfff2142-25dc-4c50-a4c5-000eb9f652c5",
        "essential": true
      },
      "paymentId": {
        "value": "371065fa-f134-4a43-815a-efa741407541",
        "essential": true
      },
      "acr": {
        "essential": true,
        "values": [
          "urn:openbanking:psd2:sca",
          "urn:openbanking:psd2:ca"
        ]
      }
    }
  }
}
.
<signature>
FieldTypeRequiredLegend    
typstringtrueType of the token. Always gets value JWT.    
algstringtrueThe signing algorithm used. Possible values are RS256 and ES256.    
kidstringtrueID of the signing key. A key corresponding to this value must be available at a public JWKS endpoint. If you have used the registration app in sandbox, this value is found in the field ssaSigningKid in registration-result.txt (the key is in ssa-signing-key.pem).    
audstringtrueIntended audience of the JWT, i.e. the host URL of the API.    
issstringtrueIssuer of the token, i.e. Client ID of the TPP.    
response_typestringtrueSpecifies the type of the authorization flow. Allowed values for the default authorization flow are code and code id_token. When using the exemption flow, the value must be token.    
client_idstringtrueUnique identifier of the client application.    
redirect_uristringDefault flow: true, Exemption flow: falseThe address to which the end user will be redirected after authentication and authorization. The access token or error descriptors will be attached to this uri as query parameters.    
scopestringtrueScope names that identify the extent of the requested authorization, as a list of space-separated names. Per the OIDC specification all requests (exemption flow notwithstanding) must contain the openid scope.    
statestringtrueA string that will be returned to the TPP. Used for validating responses and correlating requests with responses. Must only contain supported characters specified in RFC 4648 Section 5, Table 2.    
noncestringtrueA unique string that is used for protecting against replay attacks. Generated by the Client App.    
max_agenumbertrueMaximum allowed age for the authentication, in seconds.expnumbertrueToken expiry timestamp
iatnumbertrueToken issued at timestamp    
claimsobjecttrueContains information related to the request. The field authorizationId contains the authorizationId obtained while registering intent. The field paymentId must contain the paymentId to be cancelled.    
userinfoobjecttrueContains information related to the request. The field authorizationId contains the authorizationId obtained while registering intent. The field paymentId must contain the paymentId to be cancelled.    
id_tokenobjecttrueThe contents of this field are similar to userinfo.    
acrobjtrueSpecifies the requested authorization contexts. In the default flow, the value must be urn:openbanking:psd2:sca.    

Once the body and header have been constructed, follow these steps:

  1. Base64 url encode the header and body separately. Append the body to the header, separating the two parts with a dot.
  2. Create a signature by taking a copy of the newly created string and encrypting it with your private key (from QSEALC or emulated) using the algorithm specified in the header. This is the signature.
  3. Attach the signature to the encoded header and body with a dot. You'll get something like this:
eyJhbGciOiJIUzI1NiJ9.eyJtc2ciOiJIZWxsbyJ9.N6-M8oOwCq9x820PR6HMtWo-_FL5GeWUSqQ66H2F5kA

Remember to leave out padding characters ("=").

See our example code for signing JWTs. This code assumes that you have private and public JWKS readily available. In the sandbox, developers may use the Registration Helper App to auto-generate JWKSs.

ES256 or RS256 are allowed as signing algorithms.

import * as fs from "fs";
import { JWKS, JWK, JWT } from "jose";

//request is a valid pre-created JWT body
const asymmetricSign = (request: object, privateJwksPath: string) => {
  const privateJwks = JSON.parse(
    fs.readFileSync(privateJwksPath, "utf8").toString()
  );
  const privateJwk = JWKS.asKeyStore(privateJwks, {
    calculateMissingRSAPrimes: true,
  }).get({ alg: "RS256" });
  return JWT.sign(JSON.stringify(request), privateJwk);
};

You can validate the generated JWT with your public JWKS as follows:

import * as fs from 'fs';
import { JWT } from 'jose';

const publicJwks = JWKS.asKeyStore(JSON.parse(fs.readFileSync('path/to/publicjwks.json', 'utf8').toString()));

JWT.verify(jwt, publicJwks, {
    algorithms: \['ES256', 'RS256', 'PS256'],
    clockTolerance: '120s',
    maxTokenAge: '60s',
    complete: true,
});

Learn more about JWTs on jwt.io. Note that the JWT editor is currently incapable of processing this type of key.

1.2 Redirection

Once the request is complete, the TPP will redirect the customer to OP's auth server. The client app must append the following query parameters to the redirection URI:

  • request (the JWT request)
  • response_type
  • client_id
  • scope

The values of the parameters must match with those in the JWT request. Remember that all query parameters must be URL encoded.

The redirect will have the following structure:

https://authorize.psd2-sandbox.op.fi/oauth/authorize
?request=<your_JWT_string>
&client_id=******
&response_type=code%20id_token
&scope=openid%20payments%3Adelete
&state=80c00b913696c4a7c9598470

Direct the customer to this URI in the browser.

1.3 The customer authenticates and authorizes

Authentication and authorization are the responsibility of the customer and the authorization server. The customer logs in and selects the authorizations they wish to confirm or reject.

The following test credentials are available in our sandbox:

UsernamePasswordKey code / mobile key
8888888111221122
8888888211221122

1.4 Back to Client Application

The auth server will redirect the end user to the redirect URI specified in the request. There are two possible outcomes:

Success:
<REDIRECT_URI>?state=<state>&code=<authorization_code>

Success with ID token:
<REDIRECT_URI>#state=<state>&code=<authorization_code>&id_token=<id_token>

Error:
<REDIRECT_URI>?state=<state>&error=<error_description>

For example:
https://localhost:8181/oauth/access_token?error=temporarily_unavailable&state=373b23cd-a1b3-449a-810f-cd4b36f7a60c

As the user returns to the TPP's application, the TPP should verify that the returning user is the same as the user who began started the authorization process. This can be achieved by inspecting the user's session ID.

ID tokens are created and returned on two occasions:

  1. When using response type code id_token in the authorization request. The ID token will be delivered in a query parameter attached to the client app's redirect URI. Note that when an ID token is present, the query string is in fact an URI fragment (begins with "#" instead of "?").
  2. When requesting a token from a token endpoint and scope includes openid (in OP PSD2, always). The token will be returned in the response body to the token request.

The ID token contains information about the authorization, and provides an additional layer of security.

The ID token contains the following information:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "12345"
}
.
{
    "nonce": "94052e00990cc84c6accbab4",
    "authorizationId": "bfff2142-25dc-4c50-a4c5-000eb9f652c5",
    "iat": 1675754629,
    "iss": "https://authorize.psd2-sandbox.op.fi",
    "sub": "bfff2142-25dc-4c50-a4c5-000eb9f652c5",
    "exp": 1675758229,
    "acr": "urn:openbanking:psd2:sca",
    "aud": "hphza9KaLJRuvYI2bQyo"
    --ALTERNATIVE 1: AUTHORIZATION CODE FLOW---
    "s_hash": "76sa5dd",
    "c_hash": "asd097d"
    --ALTERNATIVE 2: TOKEN ENDPOINT---
    "rt_hash": "wFsWWP49hHo4sXwwVSHX-A",
    "at_hash": "bSUTLJiqrooANhJJ2X7pPw"
}
.
{
<<signature>>
}
FieldTypeLegend
issstring/URIIssuer of the token.
iattimestampToken issuance time as seconds since 1970-01-01T0:0:0Z.
substringThe authorization ID to which the ID token relates to.
acrstringAuthentication Context Class Reference, i.e. the level of authentication achieved. Currently, only urn:openbanking:psd2:sca is supported.
authorizationIdstringThe authorization ID to which the ID token relates to.
audstring/client IDIntended audience of the ID token. WIll contain the Client ID of the Client Application.
noncestringA value originally provided by the TPP in the request, used for protecting against replay attacks.
exptimestampTime of expiry, i.e. the time after which the token is no longer valid.
s_hashstring, case-sensitiveReturned with authorization code. State hash value: base64Url encoding of the left-most half of the hash of the octets of the ASCII representation of the state value, where the hash algorithm used is the hash algorithm used in the algHeader Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the code value with SHA-256, then take the left-most 256 bits and base64url encode them. The s_hashvalue is a case sensitive string.
c_hashstring, case-sensitiveReturned with authorization code. Code hash value: base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header.
rt_hashstringReturned from token endpoint. Refresh token hash value: base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the refresh_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the refresh_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The rt_hash value is a case sensitive string.
at_hashstringReturned from token endpoint. Access token hash value: base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case sensitive string.

The ID token must be validated following the OIDC spec. In addition, the TPP should do the following:

Alternative 1: ID token received in redirection URI

  • Validate the authorization ID against that received earlier.
  • Validate the signature of the ID token.
  • Validate s_hash (state hash).
  • Validate c_hash (code hash).

Alternative 2: ID token in access token response body

  • Validate the authorization ID against that received earlier.
  • Validate the signature of the ID token.
  • Validate rt_hash (refresh token validation).
  • Validate at_hash (access token validation).

See the public key for validating ID token signatures.

2. Client App exchanges an authorization code for an access token

At this point the TPP is in possession of an authorization code. This must now be exchanged for an access token.

POST /oauth/token HTTPS/1.1
Host: psd2.mtls.sandbox.apis.op.fi
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<redirect_uri>&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>

cURL:

curl -vk --key key.pem --cert client.crt https://psd2.mtls.sandbox.apis.op.fi/oauth/token -d 'grant_type=authorization_code&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>&code=wpj94iuZRzFvmHHpIFba7b1QiZweXm&redirect_uri=https%3A%2F%2Flocalhost%3A8181'

A successful request will yield a response with the following content:

{
  "access_token":"TfOJ0yOywdg8cwthvKUE",
  "refresh_token":"TYiOBpLHMWSgXsTg0SJ7TlqVMhSsWnJ6vxL6dUk9",
  "id_token": "eyJraWQiOiI2ZzNRRU9SY0hNOVdVZTZlcVowZi1wdjNEUEV1Y2NHVTRUdDFVMHo0cFIwIiwiYWxnIjoiUlMyNTYifQ.eyJydF9oYXNoIj---90ZXN0X2NsaWVudCJ9.OMxF_fyVTS0af_---e9sQXEGMHW_S7bsA",
  "scope":"openid payments:delete",
  "token_type":"bearer",
  "expires_in":3599,
}
FieldtypeLegend
access_tokenstringThis is the access token. Use as a bearer token in the Authorization header to access protected resources.
refresh_tokenstringCan be used to request additional access tokens. Valid for up to 45 days (Less if the authorization granted is shorter).
id_tokenstringA signed JWT containing information about the authorization. See ID tokens for a full description on the contents and recommended validation procedures for ID tokens.
scopestringA space-separated list of scope names describing the services to which the token grants authorization.
token_typestringToken type. Always has the value "bearer".
expires_innumberThe period for which the token is valid, in seconds.

Refreshing tokens

Refresh tokens may be used for requesting additional access tokens, as well as for renewing refresh tokens. They remain valid for a maximum of 45 days.

Regardless of the length of the authorization, the end user must re-authorize the Client Application if the application fails to renew the refresh token within 45 days.

Refresh requests have the following structure:

POST /oauth/token HTTPS/1.1
Host: psd2.mtls.sandbox.apis.op.fi
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<refresh_token>&client_id=<APP_CLIENT_ID>&client_secret=<APP_CLIENT_SECRET>

The response to this request is similar to that of a normal token request.

3. Client App cancels the payment(s)

Armed with the access token and payment IDs, the Client Application cancels each individual payment with a DELETE call to

/payments-psd2/v2/<payment type>/{paymentId}.

4. Client App polls payment status

TPP invokes the payments endpoint for querying payment status. The status of each payment can be polled using paymentId.

5. TPP verifies the response signature

As a final verification step, the TPP must inspect the detached JWS signature of the body of the payload of the header x-jws-signature.

The format of the signature is header..signature, where header is a JWS header and signature a JWS signature of a usual compact serialized JWS.

The payload received in the response body must be inserted into the middle of the JWS string in as shown:

header. + BASE64URL(received payload) + .signature

Read further details.

To finish, the TPP verifies the resulting compact serialized JWS (header.payload.signature) against a matching JWK. In the sandbox, see the JWK here.