Lean Open Banking Integration Guide
A step by step introduction to building with Lean Open Banking APIs in the Kingdom of Saudi Arabia.
Lean Environments
- Sandbox: to get started with test credentials and mock bank users and data.
- Production to launch your app with live credentials and connect with real banks.
- Lean Dashboard
RecommendedFor business continuity, we recommend you to create a new mailing group dedicated to Lean's integration (i.e:[email protected]) and add all the relevant employees to this group. Then, developers/employees can be added/invited to your sandbox account via our Developer Portal's "Team" feature/section.
Step 1: Authenticating with OAuth
OAuth secures Lean APIs and infrastructure using public and private secrets paired with short-lived access tokens, to get started, you'll need:
- An Application Dashboard account with Admin or Developer role access.
- Your Application ID and Client Secret (found under the 'Integration' tab in the Application Dashboard).
- An application with access to OAuth as an authentication method.
Scopes & The OAuth flow
OAuth is implemented to secure two channels of access to Lean. Access from your backend to Lean's APIs with scope=api, and access for your customers to the Lean Link SDK with scope=customer.<customer_id>.
In both cases the flow for creating, editing or modifying resources on the Lean platform is the same.
- Generate an access token for the request, this will return a JSON Web Token (JWT)
- Use the JWT as a Bearer token in subsequent API calls, or as an authentication for the Link SDK method call you want to make
- Tokens must be generated in your backend to avoid using the client secret in your frontend since it's vulnerable
Generating Access Tokens for Backend API Calls
curl -X POST 'https://auth.sa.leantech.me/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<LEAN_APPLICATION_ID>' \
--data-urlencode 'client_secret=<LEAN_CLIENT_SECRET>' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=api' Generating Customer Access Tokens for LinkSDK Flow
curl -X POST 'https://auth.sa.leantech.me/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<LEAN_APPLICATION_ID>' \
--data-urlencode 'client_secret=<LEAN_CLIENT_SECRET>' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=customer.<customer_id>'| Parameter | Description | Type |
|---|---|---|
| client_id | Your application ID - this can be retrieved from your Application Dashboard account. | String (UUID) |
| client_secret | Your client secret - this can be retrieved once from your Application Dashboard - subsequent retrievals will invalidate your existing client secret. | String |
| grant_type | The type of access you require a token for - currently this should always be set toclient_credentials | String |
| scope | What the scope of the access token should be, either api or customer.<customer_id> | String |
How to get client_id & client_secret
- Sign up to Lean Dashboard
- Go to Integration tab and find both paramters for your apllication as shown below:

Response
{
"access_token": "YOUR_ENCODED_JWT",
"token_type": "bearer",
"expires_in": 3599,
"scope": "api",
}
| Parameter | Description | Type |
|---|---|---|
| access_token | The access token value for use with the Lean APIs | String |
| token_type | Will always be bearer, indicates the type of token returned | String |
| expires_in | The time in seconds until the access token expires | Integer |
| scope | The scope of the access token and what resources it can access | String |
More details about authentication here.
Step 2: Create Customer
In order to start the consent journey and fetching data, you first need to create a Customer resource. This creates a relationship between your application, user and all the services they consume within Lean into a single object.
To create a Customer, call the /customers/v1 endpoint with a reference to your app_user_id.
curl -X POST 'https://sandbox.sa.leantech.me/customers/v1/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <backend_api_access_token>' \
--data-raw '{"app_user_id": "YOUR_IDENTIFIER_FOR_CUSTOMER"}'
Please note:
app_user_idhas a unique constraint, but does not need to map directly to the id of the user in your database, for example you could passprod_usr_1246as theapp_user_id, so that you can identify that the Customer is a user of your production database. In this case, you should save both thecustomer_idandapp_user_idagainst your user table for later retrieval and mapping.
As part of the response you will receive the Lean customer_id which you should save against your user table for future reference.
{
"app_user_id": "IDENTIFIER_FOR_CUSTOMER",
"customer_id": "f08fb010-878f-407a-9ac2-a7840fb56185"
}Step 3: Link SDK Consent Journey
Lean Link SDK enables you to embed the user bank connections flow directly into their applications. It's available on platforms including Web, iOS, Android, React Native and Flutter.
Check it out here
Live demo of Lean LinkSDK
The connect method is used to connect a customer to the Data API. You can use this method to generate an Entity with a single customer login.
Required fields
| Field | Type | Description |
|---|---|---|
app_token | string | Your Lean application token from the Lean dashboard. |
customer_id | string | Unique identifier of the end customer in your system. |
permissions | string[] | Permissions to request. Must contain at least one value. "accounts", "identity", "balance","transactions", "beneficiaries", "identities", "scheduled_payments", "direct_debits", "standing_orders" |
access_token | string | A valid Lean JWT with customer scope. When provided, the SDK skips its own auth step and uses this token as the Authorization: Bearer header on every API request. The JWT must include an exp claim with at least 10 minutes of remaining validity. |
success_redirect_url | string | URL to redirect to on successful completion. |
fail_redirect_url | string | URL to redirect to on failure or cancellation. |
show_consent_explanation | boolean | To set value=true to show an explanation screen before the customer grants consent. |
Lean.connect({
app_token: APP_TOKEN,
access_token: CUSTOMER_ACCESS_TOKEN,
customer_id: CUSTOMER_ID,
permissions: [
"identity",
"accounts",
"balance",
"transactions",
"beneficiaries",
],
sandbox: true,
show_consent_explanation: true,
success_redirect_url: "https://www.leantech.me/?success=true",
fail_redirect_url: "https://www.leantech.me/?success=false",
access_from: "2018-11-01T00:00:00+4:00", //optional
access_to: "2022-11-01T00:00:00+4:00", //optional
});
Check it out here
Live demo of Lean LinkSDK
Step 4: Webhooks
Webhooks are used to immediately notify your backend of events that take place in the Lean ecosystem. These are especially useful for events that take place on your front end through the Lean SDK or events that take place in data workflow like data refreshes . Once you receive an event on your server, you can process and act on it as you need.
More details about webhooks here including IP whitelisting and retry policy.
The following are the main webhooks for connecting the bank accounts of your users:
Entity created
"type": "entity.created"
This webhook is triggered when your customer successfully connects their account with the bank. The entity object is used by you to retrieve data from your customer's bank account. Where the entity_id serves as a token representing the holding bank account.
{
"type": "entity.created",
"message": "An entity object has been created.",
"payload": {
"id": "dd64bba9-9446-4ca0-b9ed-bec2b2a49024",
"app_user_id": "lean_test_framework_user_1678954843328",
"customer_id": "a8e9bf82-245a-4f5c-b048-9024fd7910cb",
"permissions": [
"transactions", "identity", "identities", "accounts",
"standing_orders", "direct_debits", "scheduled_payments", "beneficiaries"
],
"bank_details": {
"logo": "https://cdn.leantech.me/img/banks/white-lean.png",
"name": "Lean SAMA Open Banking MockBank",
"account_type": "PERSONAL",
"identifier": "LEA1_SAMAOB_SAU",
"main_color": "#1beb75",
"background_color": "#ffffff"
}
}
}Entity Data Refresh Updated
"type": "entity.data.refresh.updated"
The entity.data.refresh.updated webhooks notify the progress of fetching data for a connected entity: where a final entity.data.refresh.updated webhook with status FINISHED webhook is sent once all data types have been completed and data is available to retrieve in Lean store via API calls.
More details about data workflow here.
{
"type": "entity.data.refresh.updated",
"message": "An entity data refresh state has been updated.",
"payload": {
"refresh_id": "d4718195-fef6-43ff-a3aa-69fc257752ab",
"entity_id": "d4718195-fef6-43ff-a3aa-69fc257752ab",
"app_user_id": "consent_window_24",
"customer_id": "d4718195-fef6-43ff-a3aa-69fc257752ab",
"status": "PENDING/FINISHED",
"data_status": {
"accounts": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"identity": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"account_data": [
{
"account_id": "d4718195-fef6-43ff-a3aa-69fc257752ab",
"balance": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"identity": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"transactions": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"scheduled_payments": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"direct_debits": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"standing_orders": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"beneficiaries": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"transaction_availability": {
"start": "<DateTime>",
"end": "<DateTime>",
"complete_months": 12
}
},
{
"account_id": "b5098d49-840d-459e-9ea1-d02901af9b8c",
"balance": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"identity": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"transactions": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"scheduled_payments": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"direct_debits": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"standing_orders": "PENDING/PARTIAL/SUCCESS/FAILED/UNSUPPORTED",
"beneficiaries": "PENDING/SUCCESS/FAILED/UNSUPPORTED",
"transaction_availability": {
"start": "<DateTime>",
"end": "<DateTime>",
"complete_months": 12
}
}
]
}
}
}Entity updated
"type": "entity.updated"
This webhook is triggered when there is a change in the consents for the entity. This happens when your customer your customer reconnects the same bank account. For the first time, you will receive an entity.created webhook as mentioned above.
{
"type": "entity.updated",
"message": "An entity object has been updated.",
"payload": {
"id": "d4718195-fef6-43ff-a3aa-69fc257752ab",
"app_user_id": "lean_test_framework_user_1678955197525",
"customer_id": "22df5cab-1002-4835-bc78-5d1854b3fc69",
"permissions": [
"transactions", "identity", "identities", "accounts",
"standing_orders", "direct_debits", "scheduled_payments", "beneficiaries"
],
"bank_details": {
"logo": "https://cdn.leantech.me/img/banks/white-lean.png",
"name": "Lean SAMA Open Banking MockBank",
"identifier": "LEA1_SAMAOB_SAU",
"account_type": "PERSONAL",
"main_color": "#1beb75",
"background_color": "#ffffff"
}
}
}Step 5: Verify Entity Ownership (Recommended)
API Reference
Upon receiving entity.data.refresh.updated webhook with status FINISHED , you can start making an API call to verify entity ownership of the end-user of the connected bank account against bank records for multiple IBANs, you can use the verify entity ownership API:
Request
curl --location '/verifications/v2/entity' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"entity_id": "cdd274fc-2195-4058-a929-ec10b7470694",
"identifications": [
{
"type": "NATIONAL_ID",
"value": "1106972886"
}
]
}'Response
The
entity_ownership_verified=truemeans the passed national ID was verified as the account owner.
{
"status": "OK",
"results_id": "d4d564ab-e86b-41c4-ac9c-bf3281347ac6",
"message": "Data successfully retrieved",
"meta": null,
"timestamp": "2026-05-10T13:14:48.238271027Z",
"status_detail": null,
"verifications": {
"entity_ownership_verified": true,
"verification_method": "OPEN_BANKING",
"ibans": [
{
"iban": "SA0236DZJWMCRI65147ERH2V",
"iban_ownership_verified": true
},
{
"iban": "SA04203XGFV6Y7UWB7IYNK0I",
"iban_ownership_verified": true
}
]
}
}Step 6: Fetch Account Information Data APIs
The table lists all available open banking APIs and their API references:
Once the customer is connected to his bank and entity.data.refresh.updated webhook with statusFINISHED is received, you will be able to query a list of Financial Data & Insights APIs, analyze & utilize them accordingly:
Check it out here
Lean Postman Collections
Wait for the
entity.data.refresh.updatedwebhook with statusFINISHEDbefore making API calls to to receive sync results and avoid API timeouts.
Making your first data API call
With an Entity set up, the first API to call will normally be the data/v2/accounts endpoint. This simple GET request returns a list of the available accounts to query.
Let's look at a simple function to call the Accounts API:
curl --request GET \
--url 'https://sandbox.sa.leantech.me/data/v2/accounts?entity_id=550e8400-e29b-41d4-a716-446655440000' \
--header 'accept: application/json' \
--header 'Authorization: Bearer API_ACCESS_TOKEN'Step 7: Fetch Entities for a Customer
Get Entities for a customer
An Entity is generated whenever a customer connects a new bank account that is associated with a list of consents:
curl --request GET \
--url https://sandbox.sa.leantech.me/customers/v1/{customer_id}/entities \
--header 'accept: application/json' \
--header 'authorization: Bearer api_access_token'[
{
"id": "22730193-2a2f-4a14-a982-b185acc841d4",
"customer_id": "6005b400-8a6e-460b-9d98-6c9a9a72531b",
"bank_identifier": "LEA1_SAMAOB_SAU",
"permissions": {
"identity": true,
"accounts": true,
"balance": true,
"transactions": true,
"identities": true,
"scheduled_payments": false,
"standing_orders": false,
"direct_debits": false,
"beneficiaries": false
},
"bank_type": "RETAIL",
"created_at": "2026-04-13T13:34:37.559481Z",
"consents": [
{
"id": "8e50a1d6-53dd-4182-ba76-627322b1d7bc",
"bank_consent_id": "147352",
"bank_identifier": null,
"consent_type": null,
"consent_status": "ACTIVE",
"standard_consent_status": null,
"permissions": {
"identity": true,
"accounts": true,
"balance": true,
"transactions": true,
"identities": true,
"scheduled_payments": false,
"standing_orders": false,
"direct_debits": false,
"beneficiaries": false
},
"creation_date_time": "2026-04-13T13:34:11.819965Z",
"status_update_date_time": "2026-04-13T13:34:11.819965Z",
"expiration_date_time": "2026-04-14T13:34:11.499Z",
"transaction_from_date_time": "2025-04-10T20:00:00.601Z",
"transaction_to_date_time": "2026-04-14T13:34:11.499Z",
"accounts": [
{
"scheme_name": "IBAN",
"identification": "SA04203XGFV6Y7UWB7IYNK0I",
"name": "Mae Zulauf Current Account",
"secondary_identification": null
},
{
"scheme_name": "ACCOUNT_NUMBER",
"identification": "3XGFV6Y7UWB7IYNK0I",
"name": "Mae Zulauf Current Account",
"secondary_identification": null
}
]
}
]
}
]Step 8: Fetch Available Bank Providers
To retrieve the list of banks supported by Lean with their availability details.
Additional details:
- More details and how to create your own bank list here if that's required, otherwise the LinkSDK handles showing the banks list.
- Details about availability and bank disablement here.
Request:
curl --request GET \
--url 'https://sandbox.sa.leantech.me/banks/v1/?account_types=PERSONAL' \
--header 'accept: application/json' \
--header 'authorization: Bearer api_access_token'Response:
[
{
"identifier": "LEA2_SAMAOB_SAU",
"name": "Mockbank 2",
"arabic_name": null,
"commercial_name": "Mockbank 2",
"commercial_name_arabic": null,
"logo": "https://cdn.leantech.me/img/banks/color-gmockbank2.png",
"logo_alt": "https://cdn.leantech.me/img/banks/color-gmockbank2.png",
"main_color": "#1beb75",
"background_color": "#ffffff",
"theme": "light",
"country_code": "SAU",
"active": true,
"mock": true,
"traits": [
"auth-redirect",
"decoupled-auth-redirect"
],
"supported_account_types": [
"CREDIT",
"SAVINGS",
"CURRENT"
],
"supported_account_sub_types": [
"CURRENT",
"SAVINGS",
"CREDIT"
],
"bank_type": "RETAIL",
"swift_code": "MOCKSA03",
"transfer_limits": [],
"international_transfer_limits": [],
"international_destinations": [],
"account_type": "PERSONAL",
"connection_type": "OPEN_BANKING",
"availability": {
"active": {
"payments": false,
"data": false
},
"enabled": {
"payments": true,
"data": true
}
}
},
{
"identifier": "LEA1_SAMAOB_SAU",
"name": "Mockbank 1",
"arabic_name": null,
"commercial_name": "Mockbank 1",
"commercial_name_arabic": null,
"logo": "https://cdn.leantech.me/img/banks/color-gmockbank2.png",
"logo_alt": "https://cdn.leantech.me/img/banks/color-gmockbank2.png",
"main_color": "#1beb75",
"background_color": "#ffffff",
"theme": "light",
"country_code": "SAU",
"active": true,
"mock": true,
"traits": [
"auth-redirect",
"decoupled-auth-redirect"
],
"supported_account_types": [
"CREDIT",
"SAVINGS",
"CURRENT"
],
"supported_account_sub_types": [
"CURRENT",
"SAVINGS",
"CREDIT"
],
"bank_type": "RETAIL",
"swift_code": "MOCKSA01",
"transfer_limits": [
{
"currency": "SAR",
"min": 10,
"max": 100000
}
],
"international_transfer_limits": [],
"international_destinations": [],
"account_type": "PERSONAL",
"connection_type": "OPEN_BANKING",
"availability": {
"active": {
"payments": false,
"data": false
},
"enabled": {
"payments": true,
"data": true
}
}
}
]How disablement is represented
Each bank object contains an availability object as shown in the above response:
"availability": {
"active": {
"data": false
},
"enabled": {
"data": true
}
}availability.active→ set by Lean when a bank is disabled at the platform levelavailability.enabled→ set by clients when a bank is disabled in the Developer Portal
Both must be true for a bank to be considered available. If either active or enabled is false, the bank should not be displayed.
Additionally, when the availability of a bank is updated (enabled/ disabled) a notification via the bank.availability.updated webhook will be triggered:
{
"type": "bank.availability.updated",
"message": "The bank status has been updated.",
"payload": {
"identifier": "LEA1_SAMAOB_SAU",
"availability": {
"active": {
"data": false,
"payments": false
},
"enabled": {
"data": true,
"payments": true
}
}
},
"event_id": "56bd5dea-9745-49d0-8db0-5e1541f814cc",
"timestamp": "2026-05-14T14:13:35.151634492Z"
}Updated 2 days ago
