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.
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_jwtbefore calling the token endpoint.
End-to-end Products & Leads
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.
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.
Aggregate the LFI catalogues for display
/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
| Parameter | Type | Description |
|---|---|---|
ProductCategory | string | Filter by category — SavingsAccount, CurrentAccount, CreditCard, Finance, or Mortgage |
IsShariaCompliant | boolean | Filter to Sharia-compliant products only |
LastUpdatedDateTime | date-time | Return only products updated after this timestamp |
SortField | string | Sort by LastUpdatedDateTime (default) or ProductId |
SortOrder | string | asc (default) or desc |
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:
{
"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:
| Field | Type | Description |
|---|---|---|
ApplicationUri | string <uri> | A link to apply for the product. |
ApplicationPhoneNumber | string | A phone number to apply for the product. |
ApplicationEmail | string | An email address to apply for the product. |
ApplicationDescription | string | A 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.
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.
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:
| Field | Present | Action |
|---|---|---|
ApplicationUri | Yes | Redirect the user to this URL to complete the application on the LFI's own platform |
ApplicationPhoneNumber | Yes | Display the phone number for the user to call the LFI |
ApplicationEmail | Yes | Display the email address for the user to contact the LFI |
ApplicationDescription | Yes | Display 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.
Submit a lead on the user's behalf
/leadsIf 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.
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
| Field | Required | Description |
|---|---|---|
Email | Yes | User's email address |
EmiratesId | Yes | UAE Emirates ID number |
MarketingOptOut | Yes | Whether the user has opted out of marketing communications |
ProductCategories | Yes | One or more product categories the user is interested in |
Name | Yes | User's name — GivenName + LastName, FullName, or BusinessName for business accounts |
PhoneNumber | No | E.164 format, e.g. +971501234567 |
Nationality | No | ISO 3166-1 alpha-2 country code |
ResidentialAddress | No | Structured address including AddressLine, Country, and optionally UAE CountrySubDivision |
LeadInformation | No | Free-text notes about the lead |
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.
