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 connecting with real banks.
  • Lean Dashboard

Step 1: Authenticating with OAuth

OAuth is a standard authentication method used to secure APIs and other infrastructure using Public and Private secrets combined with short-lived access tokens. In order to get started with OAuth authentication you will need the following:

  • 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.

  1. Generate an access token for the request, this will return a JSON Web Token (JWT)
  2. 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
  3. 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>'

ParameterDescriptionType
client_idYour application ID - this can be retrieved from your Application Dashboard account.String (UUID)
client_secretYour client secret - this can be retrieved once from your Application Dashboard - subsequent retrievals will invalidate your existing client secret.String
grant_typeThe type of access you require a token for - currently this should always be set toclient_credentialsString
scopeWhat 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",
}
ParameterDescriptionType
access_tokenThe access token value for use with the Lean APIsString
token_typeWill always be bearer, indicates the type of token returnedString
expires_inThe time in seconds until the access token expiresInteger
scopeThe scope of the access token and what resources it can accessString

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_id has a unique constraint, but does not need to map directly to the id of the user in your database, for example you could pass prod_usr_1246 as the app_user_id, so that you can identify that the Customer is a user of your production database. In this case, you should save both the customer_id and app_user_id against 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.

💭

Live demo of Lean LinkSDK

Check it out here

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

FieldTypeDescription
app_tokenstringYour Lean application token from the Lean dashboard.
customer_idstringUnique identifier of the end customer in your system.
permissionsstring[]Permissions to request. Must contain at least one value. "accounts", "identity", "balance","transactions", "beneficiaries", "identities", "scheduled_payments", "direct_debits", "standing_orders"
access_tokenstringA 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_urlstringURL to redirect to on successful completion.
fail_redirect_urlstringURL to redirect to on failure or cancellation.
show_consent_explanationbooleanTo set as 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
});
💭

Live demo of Lean LinkSDK

Check it out here

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 the 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 re-connect again the same bank account again. For the first time, you will receive an entity.created webhook 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 connect 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

Theentity_ownership_verified=true represents that passed national ID as the verified 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, you will be able to query a list of Financial Data & Insights APIs, analyze & utilize them accordingly:

💭

Lean Postman Collections

Check it out here

❗️

Wait for the entity.data.refresh.updated webhook with status FINISHED before making the 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 level
  • availability.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.