Skip to content

🕒 7 minute read

Confirmation of Payee - API Guide ​ v2.1

Confirmation of Payee (CoP) lets a TPP verify that an IBAN belongs to the named individual or business before initiating a payment. Unlike payment flows, CoP does not require user authorization — the TPP authenticates directly using a client credentials grant and the LFI responds with a match result in seconds.

CoP is served by each participating LFI independently. Before calling an LFI directly, the TPP first calls the API Hub's discovery endpoint to identify which LFI holds the destination account and retrieve its endpoint URLs.

Prerequisites ​

Before calling the CoP API, ensure the following requirements are met:

  • Registered Application The application must be created within the Trust Framework and assigned the BSIP role as defined in Roles.

  • Valid Transport Certificate An active transport certificate must be issued and registered in the Trust Framework to establish secure mTLS communication.

  • Valid Signing Certificate An active signing certificate must be issued and registered in the Trust Framework. This certificate is used to sign the confirmation request JWT and client assertions.

  • Registration with the relevant Authorisation Server The application must be registered with the Authorisation Server of the LFI that holds the destination account.

  • Understanding of Tokens & Assertions You should understand how client authentication works with private_key_jwt before calling the token endpoint.

API Sequence Flow ​

Click to expand

Step 1 — Discover the LFI ​

CoP is served by individual LFIs — the /discovery endpoint resolves a payee IBAN to the correct LFI and returns two URLs you will need for the rest of the flow:

FieldDescription
DiscoveryEndpointUrlThe .well-known endpoint for the LFI's Authorisation Server. Fetch this to obtain the token_endpoint and issuer used in later steps.
ResourceServerUrlThe base URL of the LFI's Resource Server. Use this as the base URL when calling /confirmation.

Before calling /discovery you must obtain an access token from any LFI you are registered with using a client credentials grant. The API Hub does not make any requests to the LFI when processing /discovery — it resolves the IBAN centrally — so the response is the same regardless of which LFI you authenticate with. You only need to perform discovery once, and the POST /discovery request must be sent to the LFI whose token you are using.

Step 2 — Build a Client Assertion ​

Use the signJWT() helper to build a client assertion proving your application's identity:

typescript
import crypto from 'node:crypto'
import { signJWT } from './sign-jwt'

const CLIENT_ID = process.env.CLIENT_ID!
const ISSUER    = process.env.ISSUER!   // from the LFI's .well-known/openid-configuration

const clientAssertion = await signJWT({
  iss: CLIENT_ID,
  sub: CLIENT_ID,
  aud: ISSUER,
  jti: crypto.randomUUID(),
})
python
import os, uuid
from sign_jwt import sign_jwt

CLIENT_ID = os.environ["CLIENT_ID"]
ISSUER    = os.environ["ISSUER"]   # from the LFI's .well-known/openid-configuration

client_assertion = sign_jwt({
    "iss": CLIENT_ID,
    "sub": CLIENT_ID,
    "aud": ISSUER,
    "jti": str(uuid.uuid4()),
})

See Client Assertion for the full claims reference.

Step 3 — Token Request ​

POST to any LFI's token endpoint with scope=confirmation-of-payee:

typescript
const TOKEN_ENDPOINT = process.env.TOKEN_ENDPOINT!  // from the LFI's .well-known/openid-configuration

const params = new URLSearchParams({
  grant_type:            'client_credentials',
  scope:                 'confirmation-of-payee',
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
  client_assertion:      clientAssertion,
})

const tokenResponse = await fetch(TOKEN_ENDPOINT, {
  method:  'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body:    params.toString(),
  // agent: new https.Agent({ cert: transportCert, key: transportKey }),
})

const { access_token: accessToken } = await tokenResponse.json()
python
import httpx, os

TOKEN_ENDPOINT = os.environ["TOKEN_ENDPOINT"]  # from the LFI's .well-known/openid-configuration

token_response = httpx.post(
    TOKEN_ENDPOINT,
    data={
        "grant_type":            "client_credentials",
        "scope":                 "confirmation-of-payee",
        "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        "client_assertion":      client_assertion,
    },
    # cert=("transport.crt", "transport.key"),
)

access_token = token_response.json()["access_token"]

Step 4 — Build a signed discovery request ​

The request body is a signed JWT containing the IBAN, signed with your signing key:

typescript
// CLIENT_ID and ISSUER already set in Step 2

const discoveryRequest = await signJWT({
  iss: CLIENT_ID,
  aud: ISSUER,
  jti: crypto.randomUUID(),
  message: {
    Data: {
      SchemeName:     'IBAN',
      Identification: 'AE070331234567890123456',   // IBAN to check
    },
  },
})
python
# CLIENT_ID and ISSUER already set in Step 2

discovery_request = sign_jwt({
    "iss": CLIENT_ID,
    "aud": ISSUER,
    "jti": str(uuid.uuid4()),
    "message": {
        "Data": {
            "SchemeName":     "IBAN",
            "Identification": "AE070331234567890123456",   # IBAN to check
        }
    },
})

Step 5 — POST /discovery ​

Include x-fapi-interaction-id on the request. See Request Headers.

typescript
const LFI_BASE_URL = process.env.LFI_BASE_URL!  // base URL of the LFI you authenticated with in Step 3
// accessToken obtained in Step 3

const discoveryResponse = await fetch(
  `${LFI_BASE_URL}/open-finance/confirmation-of-payee/v2.1/discovery`,
  {
    method:  'POST',
    headers: {
      'Authorization':         `Bearer ${accessToken}`,
      'Content-Type':          'application/jwt',
      'Accept':                'application/jwt',
      'x-fapi-interaction-id': crypto.randomUUID(),
    },
    body: discoveryRequest,
    // agent: new https.Agent({ cert: transportCert, key: transportKey }),
  }
)

// Response is a signed JWT — decode the payload to read the result
const discoveryJwt     = await discoveryResponse.text()
const [, discoveryB64] = discoveryJwt.split('.')
const { message }      = JSON.parse(Buffer.from(discoveryB64, 'base64url').toString())

const { DiscoveryEndpointUrl, ResourceServerUrl } = message.Data
python
import httpx, base64, json, os

LFI_BASE_URL = os.environ["LFI_BASE_URL"]  # base URL of the LFI you authenticated with in Step 3
# access_token obtained in Step 3

discovery_response = httpx.post(
    f"{LFI_BASE_URL}/open-finance/confirmation-of-payee/v2.1/discovery",
    headers={
        "Authorization":         f"Bearer {access_token}",
        "Content-Type":          "application/jwt",
        "Accept":                "application/jwt",
        "x-fapi-interaction-id": str(uuid.uuid4()),
    },
    content=discovery_request,
    # cert=("transport.crt", "transport.key"),
)

# Response is a signed JWT — decode the payload to read the result
discovery_jwt = discovery_response.text
payload_b64   = discovery_jwt.split(".")[1]
message       = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))["message"]

discovery_endpoint_url = message["Data"]["DiscoveryEndpointUrl"]
resource_server_url    = message["Data"]["ResourceServerUrl"]

See the POST /discovery API reference for the full request and response schema.

Step 6 — Resolve the LFI token endpoint ​

Fetch the DiscoveryEndpointUrl directly to read the LFI's OpenID configuration. This gives you the token_endpoint and issuer needed for the next steps:

typescript
const oidcConfig    = await fetch(DiscoveryEndpointUrl).then(r => r.json())
const tokenEndpoint = oidcConfig.token_endpoint   // used in Step 8
const issuer        = oidcConfig.issuer           // used in Step 7
python
oidc_config    = httpx.get(discovery_endpoint_url).json()
token_endpoint = oidc_config["token_endpoint"]   # used in Step 8
issuer         = oidc_config["issuer"]           # used in Step 7

Step 7 — Build a Client Assertion ​

Use the signJWT() helper to build a client assertion proving your application's identity:

typescript
import crypto from 'node:crypto'
import { signJWT } from './sign-jwt'

const CLIENT_ID = process.env.CLIENT_ID!
// issuer resolved from DiscoveryEndpointUrl in Step 6

const clientAssertion = await signJWT({
  iss: CLIENT_ID,
  sub: CLIENT_ID,
  aud: issuer,
  jti: crypto.randomUUID(),
})
python
import os, uuid
from sign_jwt import sign_jwt

CLIENT_ID = os.environ["CLIENT_ID"]
# issuer resolved from discovery_endpoint_url in Step 6

client_assertion = sign_jwt({
    "iss": CLIENT_ID,
    "sub": CLIENT_ID,
    "aud": issuer,
    "jti": str(uuid.uuid4()),
})

See Client Assertion for the full claims reference.

Step 8 — Token Request ​

POST to the the token endpoint (resolved in Step 6) with scope=confirmation-of-payee:

typescript
// tokenEndpoint resolved from DiscoveryEndpointUrl in Step 6

const params = new URLSearchParams({
  grant_type:            'client_credentials',
  scope:                 'confirmation-of-payee',
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
  client_assertion:      clientAssertion,
})

const tokenResponse = await fetch(tokenEndpoint, {
  method:  'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body:    params.toString(),
  // agent: new https.Agent({ cert: transportCert, key: transportKey }),
})

const { access_token } = await tokenResponse.json()
python
import httpx

# token_endpoint resolved from discovery_endpoint_url in Step 6

token_response = httpx.post(
    token_endpoint,
    data={
        "grant_type":            "client_credentials",
        "scope":                 "confirmation-of-payee",
        "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        "client_assertion":      client_assertion,
    },
    # cert=("transport.crt", "transport.key"),
)

access_token = token_response.json()["access_token"]

POST /open-finance/confirmation-of-payee/v2.1/confirmation ​

Step 9 — Build and Sign the Confirmation Request ​

The confirmation request is sent as a signed JWT (Content-Type: application/jwt). Build the JWT payload containing the account details you want to verify, then sign it with your signing key.

Request payload fields ​

FieldTypeDescriptionExample
Data.SchemeName*enumAccount identifier type — always IBANIBAN
Data.Identification*stringThe IBAN to verifyAE070331234567890123456
Data.Name.FullName*stringFull name of the account holderIbrahim Al Suwaidi
Data.Name.GivenNamestringGiven (first) name — individual accountsIbrahim
Data.Name.LastNamestringFamily name — individual accountsAl Suwaidi
Data.Name.BusinessNamestringRegistered business name — use instead of personal name fields for business accountsBusiness Inc.

Individual vs. Business

Provide GivenName + LastName for personal accounts, or BusinessName for business accounts. Do not mix both.

Example payload (inside the JWT message claim) ​

json
{
  "Data": {
    "SchemeName": "IBAN",
    "Identification": "AE070331234567890123456",
    "Name": {
      "FullName": "Ibrahim Al Suwaidi",
      "GivenName": "Ibrahim",
      "LastName": "Al Suwaidi"
    }
  }
}

Signing the request ​

Use the signJWT() helper, wrapping the payload in a message claim:

typescript
import crypto from 'node:crypto'
import { signJWT } from './sign-jwt'

const signedRequest = await signJWT({
  iss: CLIENT_ID,
  aud: issuer,
  jti: crypto.randomUUID(),
  message: {
    Data: {
      SchemeName:     'IBAN',
      Identification: 'AE070331234567890123456',
      Name: {
        FullName:  'Ibrahim Al Suwaidi',
        GivenName: 'Ibrahim',
        LastName:  'Al Suwaidi',
      },
    },
  },
})
python
import uuid
from sign_jwt import sign_jwt

signed_request = sign_jwt({
    "iss": CLIENT_ID,
    "aud": issuer,
    "jti": str(uuid.uuid4()),
    "message": {
        "Data": {
            "SchemeName":     "IBAN",
            "Identification": "AE070331234567890123456",
            "Name": {
                "FullName":  "Ibrahim Al Suwaidi",
                "GivenName": "Ibrahim",
                "LastName":  "Al Suwaidi",
            },
        }
    },
})

Step 10 — POST /confirmation ​

Send the signed JWT to the LFI's CoP endpoint using the ResourceServerUrl resolved in Step 5. Both the request body and the response are JWTs. Include x-fapi-interaction-id on every request. See Request Headers.

typescript
// ResourceServerUrl resolved from discovery in Step 5

const copResponse = await fetch(
  `${ResourceServerUrl}/open-finance/confirmation-of-payee/v2.1/confirmation`,
  {
    method:  'POST',
    headers: {
      'Authorization':       `Bearer ${access_token}`,
      'Content-Type':        'application/jwt',
      'Accept':              'application/jwt',
      'x-fapi-interaction-id': crypto.randomUUID(),
    },
    body: signedRequest,
    // agent: new https.Agent({ cert: transportCert, key: transportKey }),
  }
)

// Response is a signed JWT — decode the payload to read the result
const responseJwt   = await copResponse.text()
const [, payloadB64] = responseJwt.split('.')
const result = JSON.parse(Buffer.from(payloadB64, 'base64url').toString())
python
import httpx, base64, json

# resource_server_url resolved from discovery in Step 5

cop_response = httpx.post(
    f"{resource_server_url}/open-finance/confirmation-of-payee/v2.1/confirmation",
    headers={
        "Authorization":         f"Bearer {access_token}",
        "Content-Type":          "application/jwt",
        "Accept":                "application/jwt",
        "x-fapi-interaction-id": str(uuid.uuid4()),
    },
    content=signed_request,
    # cert=("transport.crt", "transport.key"),
)

# Response is a signed JWT — decode the payload to read the result
response_jwt  = cop_response.text
payload_b64   = response_jwt.split(".")[1]
result        = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))

Response ​

The response is a signed JWT. Decode the payload to read the match result:

FieldTypeDescription
Data.NameMatchIndicatorstringThe result of the name match check — see enum below
Data.MaskedNamestringThe account holder's name, partially masked. Returned on ConfirmationOfPayee.Partial and ConfirmationOfPayee.No
NameMatchIndicatorMeaning
ConfirmationOfPayee.YesName and account match — safe to proceed
ConfirmationOfPayee.PartialName partially matches — present the MaskedName to the payer
ConfirmationOfPayee.NoName does not match — present the MaskedName to the payer

Proceed with caution on non-Yes results

A ConfirmationOfPayee.Partial or ConfirmationOfPayee.No result must be surfaced to the payer — along with the MaskedName — before initiating a payment. Proceeding without informing the user may increase the risk of authorised push payment fraud.

Decoding the JWS ​

The /confirmation response body is a compact JWS — three base64url-encoded segments separated by .:

<header>.<payload>.<signature>

Verify the signature using the LFI's public key, then base64url-decode the payload:

typescript
function decodeJwsPayload(jws: string) {
  const [, payloadB64] = jws.split('.')
  const json = atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/'))
  return JSON.parse(json)
}

The decoded payload contains a message object with the CoP result under message.Data:

json
{
  "iss": "https://rs1.altareq1.sandbox.apihub.openfinance.ae",
  "aud": ["https://tpp.example.com"],
  "iat": 1713196200,
  "nbf": 1713196200,
  "exp": 1713196500,
  "message": {
    "Data": {
      "NameMatchIndicator": "ConfirmationOfPayee.Partial",
      "MaskedName": "Ibrahim Al S*****"
    },
    "Links": {
      "Self": "https://rs1.altareq1.sandbox.apihub.openfinance.ae/open-finance/confirmation-of-payee/v2.1/confirmation"
    },
    "Meta": {}
  }
}

See the POST /confirmation API reference for the full request and response schema.

Where Confirmation of Payee has been performed for a creditor, include the full raw JWS response string returned by the /confirmation endpoint in the ConfirmationOfPayeeResponse field of the creditor entry inside the payment consent PII.

json
{
  "Initiation": {
    "Creditor": [
      {
        "Creditor": { "Name": "Ibrahim Al Suwaidi" },
        "CreditorAccount": {
          "SchemeName": "IBAN",
          "Identification": "AE070331234567890123456",
          "Name": { "en": "Ibrahim Al Suwaidi" }
        },
        "ConfirmationOfPayeeResponse": "eyJhbGci..."   // full JWS string from Step 10
      }
    ]
  }
}

This gives the LFI confidence that the creditor account details have been verified before the payment consent was created. The value must be the complete compact JWS string — do not decode it to an object before embedding.

See Creditor for the full PII creditor schema and the creditor models (single, multiple, and open beneficiary).

See Confirmation of Payee — User Experience for consent and authorisation page examples across different match results.