TPP · Banking · Products & Leads

Products & Leads — API Guide 3 min read

The Products & Leads API lets a TPP retrieve publicly available banking products from participating LFIs and present them to a user. Products are fetched from each LFI individually and aggregated for display. No user consent or redirect is required — the TPP authenticates directly with a client credentials grant.

Once the user selects a product they have two options: Apply Now, which directs them to the LFI using whichever application channel the LFI has configured, or Request contact from bank, which submits a lead to the LFI on the user's behalf.

01 Prerequisites

What you need before calling the Products & Leads API

Before calling the Products & Leads API, ensure the following requirements are met:

  • Registered Application — the application must be created within the Trust Framework and assigned the BDSP 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 for client authentication.
  • Understanding of Tokens & Assertions — you should understand how client authentication works with private_key_jwt before calling the token endpoint.
02 API Sequence Flow

End-to-end Products & Leads

Sequence diagramProducts & Leads API FlowClick to expand
03 Steps 1 & 2 — Token request per LFI (in parallel)

Get an access token from every LFI you intend to query

Each LFI has its own authorisation server, so the TPP must obtain a separate access token for every LFI it intends to query. Because the token endpoint and aud claim differ per LFI, a new client assertion must also be built for each one.

These calls should all be made in parallel — do not wait for one LFI's token before requesting the next.

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

const CLIENT_ID = process.env.CLIENT_ID!

// lfis — list of { lfiId, issuer, tokenEndpoint } from the Trust Framework Directory
const tokens = await Promise.all(
  lfis.map(async lfi => {
    const clientAssertion = await signJWT({
      iss: CLIENT_ID,
      sub: CLIENT_ID,
      aud: lfi.issuer,
      jti: crypto.randomUUID(),
    })

    const res = await fetch(lfi.tokenEndpoint, {
      method:  'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type:            'client_credentials',
        scope:                 'products',
        client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
        client_assertion:      clientAssertion,
      }).toString(),
      // agent: new https.Agent({ cert: transportCert, key: transportKey }),
    })

    const { access_token } = await res.json()
    return { lfiId: lfi.lfiId, apiBase: lfi.apiBase, access_token }
  })
)

See Client Assertion for the full claims reference.

04 Step 3 — GET /products per LFI (in parallel)

Aggregate the LFI catalogues for display

GET/products

With a token for each LFI, call GET/products for all of them simultaneously. Aggregate the results into a single list before presenting them to the user.

Include x-fapi-interaction-id and x-fapi-customer-ip-address on every request. The x-fapi-customer-ip-address header is required because GET/products can only be called while a customer is in a live session with the TPP. See Request Headers.

Each LFI's apiBase is its API Hub resource server — https://rs1.<lfiCode>.apihub.openfinance.ae (production) or https://rs1.<lfiCode>.sandbox.apihub.openfinance.ae (sandbox). Resolve the <lfiCode> from the Trust Framework Directory. See API Resources for the full endpoint format.

Query parameters

ParameterTypeDescription
ProductCategorystringFilter by category — SavingsAccount, CurrentAccount, CreditCard, Finance, or Mortgage
IsShariaCompliantbooleanFilter to Sharia-compliant products only
LastUpdatedDateTimedate-timeReturn only products updated after this timestamp
SortFieldstringSort by LastUpdatedDateTime (default) or ProductId
SortOrderstringasc (default) or desc
typescript
import crypto from 'node:crypto'

// tokens — output from Steps 1 & 2: [{ lfiId, apiBase, access_token }]
const results = await Promise.all(
  tokens.map(lfi =>
    fetch(`${lfi.apiBase}/open-finance/product/v2.1/products`, {
      headers: {
        'Authorization':              `Bearer ${lfi.access_token}`,
        'x-fapi-interaction-id':      crypto.randomUUID(),
        'x-fapi-customer-ip-address': customerIpAddress,
      },
      // agent: new https.Agent({ cert: transportCert, key: transportKey }),
    }).then(r => r.json())
  )
)

// Flatten all LFI product lists into a single array for display
const allProducts = results.flatMap(r => r.Data ?? [])

Response structure

Products are returned grouped by LFI. The Data array groups products by LFIId:

response bodyjson
{
  "Data": [
    {
      "LFIId": "ADCB",
      "LFIBrandId": "ADCB",
      "Products": [
        {
          "ProductId": "SAV-001",
          "ProductName": "Personal Savings Account",
          "ProductCategory": "SavingsAccount",
          "IsShariaCompliant": false,
          "Description": "An everyday savings account with competitive rates.",
          "DenominationCurrency": "AED",
          "ApplicationUri": "https://www.adcb.com/apply/savings",
          "KfsUri": "https://www.adcb.com/docs/savings-kfs.pdf",
          "Eligibility": {
            "ResidenceStatus": ["UaeResident"],
            "CustomerType": ["Retail"],
            "Age": [{ "Type": "MinimumAge", "Value": 18 }]
          }
        },
        {
          "ProductId": "SAV-002",
          "ProductName": "Al Hilal Savings",
          "ProductCategory": "SavingsAccount",
          "IsShariaCompliant": true,
          "ShariaStructure": "Murabaha",
          "AlternativeBrandName": "Al Hilal Savings",
          "Description": "A Sharia-compliant savings account.",
          "DenominationCurrency": "AED"
        }
      ]
    }
  ],
  "Links": { "Self": "https://api.lfi.ae/open-finance/product/v2.1/products" },
  "Meta": { "TotalPages": 1 }
}

Application fields

At least one of the following fields must be returned by the LFI for every product. This determines how the TPP presents the page for the end user to apply for their selected product:

FieldTypeDescription
ApplicationUristring <uri>A link to apply for the product.
ApplicationPhoneNumberstringA phone number to apply for the product.
ApplicationEmailstringAn email address to apply for the product.
ApplicationDescriptionstringA free text description of the application process for the product, with ways to contact the LFI if applicable.

Step 4 below covers how the TPP should respond to each of these fields when the user chooses to apply.

Displaying products

Use the LFI's logo and brand name from the Trust Framework Directory. Do not rank or order products based on commercial agreements with specific LFIs — ordering must reflect the user's own preferences.

See the GET /products API reference for the full response schema.

05 Step 4 — Apply Now

Channel the user to the LFI's application

When a user selects a product and chooses to apply, the action depends on which application fields the LFI has populated. Check the fields in priority order:

FieldPresentAction
ApplicationUriYesRedirect the user to this URL to complete the application on the LFI's own platform
ApplicationPhoneNumberYesDisplay the phone number for the user to call the LFI
ApplicationEmailYesDisplay the email address for the user to contact the LFI
ApplicationDescriptionYesDisplay the free-text description of the LFI's application process

An LFI may provide more than one of these fields. ApplicationUri is the preferred channel where available; the others provide fallback options for LFIs that do not have a direct online application.

06 Step 5 — POST /leads

Submit a lead on the user's behalf

POST/leads

If the user instead chooses to request that the LFI contact them, the TPP submits a lead. The API Hub forwards it to the LFI and does not retain the data.

As with GET/products, include x-fapi-customer-ip-address on every request — leads can only be submitted while a customer is in a live session with the TPP.

typescript
import crypto from 'node:crypto'

const leadResponse = await fetch(
  `${API_BASE}/open-finance/product/v2.1/leads`,
  {
    method:  'POST',
    headers: {
      'Authorization':              `Bearer ${access_token}`,
      'Content-Type':               'application/json',
      'x-fapi-interaction-id':      crypto.randomUUID(),
      'x-fapi-customer-ip-address': customerIpAddress,
    },
    body: JSON.stringify({
      Data: {
        Email:             '[email protected]',
        EmiratesId:        '784-1990-1234567-1',
        PhoneNumber:       '+971501234567',
        MarketingOptOut:   false,
        ProductCategories: ['SavingsAccount'],
        Name: {
          GivenName: 'Ibrahim',
          LastName:  'Al Suwaidi',
        },
      },
    }),
    // agent: new https.Agent({ cert: transportCert, key: transportKey }),
  }
)

const { Data: { LeadId } } = await leadResponse.json()  // HTTP 201

Lead request fields

FieldRequiredDescription
EmailYesUser's email address
EmiratesIdYesUAE Emirates ID number
MarketingOptOutYesWhether the user has opted out of marketing communications
ProductCategoriesYesOne or more product categories the user is interested in
NameYesUser's name — GivenName + LastName, FullName, or BusinessName for business accounts
PhoneNumberNoE.164 format, e.g. +971501234567
NationalityNoISO 3166-1 alpha-2 country code
ResidentialAddressNoStructured address including AddressLine, Country, and optionally UAE CountrySubDivision
LeadInformationNoFree-text notes about the lead
User consent

Only submit a lead when the user has explicitly opted in to being contacted by the LFI. The MarketingOptOut field must accurately reflect the user's marketing preferences.

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