Our Tech Blog

# Dynamic client registration via bank's API with eIDAS certificates

Dynamic TPP registration in Open Banking

In most cases to use open banking APIs besides eIDAS certificates a TPP would need to have addition assets, such as client ID, API key, secret key or similar. These additional assets help ASPSP (bank) to identify TPP when it is accessing PSD2 (open banking) APIs and provides additional security measure (in case private keys of TPP's eIDAS certificates got compromised).

Usually the set of the assets depends on the platform used by ASPSP to provide the APIs and/or certain open banking standard according to which the APIs are implemented. In many cases these assets can only be acquired manually via bank's developer portal; a TPP is supposed to sign up for the developer portal using web interface and in the profile UI do certain actions, such as creation of an application and selection of APIs to be used. This leads to the problem that in order to access open banking (PSD2) APIs of multiple ASPSPs (banks) TPPs are required to make significant number of manual actions.

Dynamic client registration helps to avoid manual actions preceding usage of the APIs by providing dedicated APIs for creation of client ID and similar assets. The only requirement to use these APIs is to have valid eIDAS certificates (in sandbox environments test certificates shall be used). A number of different implementations are used across banks in Europe, but in this post we are focusing on Open Banking UK standard for dynamic client registration, which is the most used.

# Background

As mentioned in the introduction the registration is done by sending an HTTP-request with desired client data to the authorization server. The registration response contains a client credentials to be used when accessing account information or payment initiation APIs.

In this post we are going through the basic concepts of dynamic client registration and provide code snippets in Python. A number of abbreviations is used throughout the post, you can consult with the glossary at the end of the article, in case you are unfamiliar with some.

Prior to the dynamic client registration process you would need to obtain QWAC (mTLS) and QSeal (signing) certificates. For production use you would need to have the certificates issued by a QTSP, but in sandbox environments many banks allow to use even self-signed certificates. Previously we wrote how to generate self-signed certificate using OpenSSL.

Open Banking UK standard provides detailed description of the dynamic TPP registration process (how to register in bank's API) and it can be found here (opens new window). The standard itself is based on OAuth 2.0 dynamic client registration protocol (opens new window).

The registration API endpoint supports HTTP-methods:

  • POST: register a new client
  • GET: get client information
  • PUT: update client information
  • DELETE: delete client information

We will focus on the first method.

# Prerequisites

All examples below are written using Python and following dependencies:


For this tutorial we consider that our bank's URL is https://example-bank.com and our company's URL is https://example-company.com

We also have our certificates files according to the following scheme:


Some aspects may vary between banks (even declaring to implement their APIs according to the Open Banking UK standard), so the examples may need to be a little bit adjusted.

# Step-by-step guide

  1. First thing you need to do is to convert your qseal.key (in PEM format) to a JWKS format.
    It is possible to use some online or ready-made tools for this purpose.
    For example, you can use this (opens new window) docker image.
    Note that you need public representation of your key. Be careful not to expose any key private data!
    We are going to save it under qseal.jwks
    The result should look something like this:
    "kty": "RSA",
    "e": "AQAB",
    "kid": "11111111-2222-aaaa-bbbb-ccccccff3344",
    "n": "SGVyZSBpcyBzb21lIGluZm9ybWF0aW9uIG9mIG91ciBwcml2YXRlIGtleQ"
  1. You are also need to create same format file but with list of revoked certificates. We will save it under qseal-revoked.jwks
    If you don't have revoked certificates, you can create an empty file:
    "keys": []
  1. Next, we are going to create a software statement.
    RFC 7591 defines a Software Statement as:

A digitally signed or MACed JSON Web Token (JWT) that asserts metadata values about the client software. In some cases, a software statement will be issued directly by the client developer. In other cases, a software statement will be issued by a third-party organization for use by the client developer. In both cases, the trust relationship the authorization server has with the issuer of the software statement is intended to be used as an input to the evaluation of whether the registration request is accepted. A software statement can be presented to an authorization server as part of a client registration request.

Below are functions we are going to use in order to create JWT and sign it.
You need to replace urls and path to a private key on your computer

import base64
import datetime
import json
import os
import uuid

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
import requests

def sign(payload: str) -> str:
    key_content = open('qseal/qseal.key', 'rb').read()  # you probably need to replace this path
    backend = default_backend()
    key = backend.load_pem_private_key(key_content, None)
    payload_encoded: bytes = payload.encode('utf-8')
    signature = key.sign(payload_encoded, padding.PKCS1v15(), hashes.SHA256())
    return base64\
            .replace('=', '')\
            .replace('/', '_')\
            .replace('+', '-')

def get_software_statement() -> str:
    header = {
        'alg': 'RS256',
        'kid': '11111111-2222-aaaa-bbbb-ccccccff3344',  # our private key id
        'typ': 'JWT'

    timestamp = int(datetime.datetime.now().timestamp())
    jti = str(uuid.uuid4())
    print(f'jti is: {jti}')
    software_id = str(uuid.uuid4())
    print(f'software_id is: {software_id}')
    countries = ['UK']

    body = {
        'iss': 'PSDUK-UKBXXX-12345678',  # Global unique reference number assigned to your company along with issued certificates
        'iat': timestamp,
        'exp': timestamp + 60 * 10,
        'aud': 'https://example-bank.com',
        'jti': jti,
        'software_id': software_id,
        'software_client_name': 'Company Name',
        'software_version': 1,
        'scope': [
                "role": "AIS",
                "member_state": countries
                "role": "PIS",
                "member_state": countries
        'software_contacts': [
                "name": "First Second",
                "email": "example@example-company.com",
                "phone": "+1234567890",
                "type": "Business"
        'software_redirect_uris': [
            'https://example-company.com/auth_redirect/'  # link to your company's website redirect url
        'software_client_uri': 'https://example-company.com/',
        'software_logo_uri': 'https://example-company.com/company-logo.png',  # link to a company logo
        'software_tos_uri': 'https://example-company.com/tos/',  # link to a company's terms of service
        'software_policy_uri': 'https://example-company.com/policy/',  # link to a company's policy
        'software_jwks_endpoint': 'https://example-company.com/qseal.jwks',  # link to a created on the step 2 certificate
        'software_jwks_revoked_endpoint': 'https://example-company.com/qseal-revoked.jwks'  # link to a created on the step 3 certificate

    header_base64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode()
    header_base64 = header_base64.replace('=', '')
    body_base64 = base64.urlsafe_b64encode(json.dumps(body).encode()).decode()
    body_base64 = body_base64.replace('=', '')

    signature = sign(f'{header_base64}.{body_base64}')
    return f'{header_base64}.{body_base64}.{signature}'
  1. After we created a software statement, we are ready to construct a registration request:
def register() -> requests.Response:
    payload = {
        "token_endpoint_auth_method": "client_secret_post",
        "grant_types": ["authorization_code", "client_credentials"],
        "software_statement": get_software_statement(),
        "id_token_signed_response_alg": "RS256",
        "request_object_signing_alg": "RS256"

    response = requests.post(
        'https://example-bank.com/register',  # registration endpoint
        cert=('qwac/qwac.crt', 'qwac/qwac.key')  # paths to a qwac certificate and a key
    return response
  1. Finally, execute the request:
register_data = register()

If our request was successfull then response should look something like this:

    "client_secret": "some_secret",
    "client_id": "some_id",
    "redirect_uris": [
    "grant_types": [
    "token_endpoint_auth_method": "client_secret_post",
    "logo_uri": "https://example-company.com/company-logo.png",
    "jwks_uri": "https://example-company.com/qseal.jwks",
    "software_roles": [
            "role": "AIS",
            "member_state": [
            "role": "PIS",
            "member_state": [
    "id_token_signed_response_alg": "RS256",
    "request_object_signing_alg": "RS256",
    "software_client_name": "Example Company",
    "scope": "openid accounts payments"

Now you can use client_id and client_secret to access other open banking API endpoints!

# Glossary

  • ASPSP - Account Servicing Payment Service Provider, i.e. a bank
  • eIDAS certificateX.509 public key certificate containing certain fields
  • JWKS – JSON Web Key Set
  • JWT – JSON Web Token
  • QWAC – Qualified Website Application Certificate, i.e. client TLS certificate
  • QSealC – Qualified Electronic Sealing Certificate, i.e. certificate used for signing API requests
  • PSU – Payment Service User, i.e. end-user / bank’s customer
  • SSA – Software Statement Assertion
  • TPP – Third party provider, i.e. a service using banks’ APIs