Data workflow
Overview
Lean has optimized the data ingestion flow to reduce the need for repeated bank-side calls and improve data availability timing.
Upon receiving the entity.created and entity.updated webhooks, Lean now immediately begins fetching and populating all available data into its internal store. This means clients no longer need to actively poll, implement the async failover for timeouts or trigger fetches for each data type - instead, data becomes queryable once the entity.data.refresh.updated webhook signals a FINISHED status. These changes are fully backward compatible and designed to simplify integration logic while preserving current API behaviour for in-progress and partial data requests.
How the platform works
Lean has simplified the integration process by consuming all available data and populating that data into our own store upon completion of the entity.created and entity.updated events. This reduces the need for roundtrip calls to banks per request and retries failed requests automatically.
These changes have been made to be fully backward compatible with our existing expected implementation flow.
Workflow
Expected workflow
sequenceDiagram
participant Client App
participant Client Backend
participant Lean API
participant Lean Store
participant Lean Webhooks
Client App->>Client Backend: Request Customer Access Token
Client Backend->>Lean API: POST /auth
Lean API-->>Client Backend: Access Token
Client Backend-->>Client App: Return Token
Client App->>Client App: Initiate LinkSDK with Token
Client App->>Lean API: User completes account connection
Lean Webhooks-->>Client Backend: entity.created
Lean Store->>Lean Store: Begin async data fetch + populate store
loop Until All Data Ready
Lean Webhooks-->>Client Backend: entity.data.refresh.updated (PENDING)
end
Lean Webhooks-->>Client Backend: entity.data.refresh.updated (FINISHED)
Client Backend->>Lean API: GET /data/v2, /insights/v2
Lean API->>Lean Store: Fetch requested data
Lean API-->>Client Backend: Return populated data
- Client registers a
Customerresource for a user on their platform.POST /customers - User lands on screen in Client App that can initiate the LinkSDK
- Client Frontend calls Client Backend for a
CustomerAccess Token.POST auth.leantech.me - Client adds the Auth token to the configuration and initiation of the LinkSDK.
.Connect() - User completes the account connection process.
- Lean sends
entity.createdwebhook - and starts populating data into store. - Lean sends
entity.data.refresh.updatedwebhooks with statusPENDINGto notify progress of population. - Optional: Client begins making requests for completed data
- Lean sends a final
entity.data.refresh.updatedwebhook with statusFINISHEDonce all requests have been completed and data is available in store. - Client begins making requests for data.
GET /data/v2GET /insights/v2
What happens to requests for data still being fetched?Lean will return
PENDINGfor any requests that do not populate within 50 seconds of the request being made. These will include aresults_id, a correspondingresults.readywebhook and can be picked up from the/resultsendpoint when ready as is the behaviour today.This enables you to queue up relatively safe API calls such as getting accounts and balances - whilst also providing a predictable maximum timeout period.
The 50 second maximum timeout is subject to change.
How long is data available for in the Store?The Data Store retains data for 30 days before being securely deleted from Lean’s system.
The 30 day period is subject to change.
Webhooks
entity.created/updated handling
entity.created/updated handlingClients should only use this webhook for completion handling moving forward, and not use it to trigger data requests.
entity.data.refresh.updated handling
entity.data.refresh.updated handling- Log into dev.leantech.me and navigate to the webhooks tab
- Add a subscription to the
entity.data.refresh.updatedwebhook to an endpoint you specify. - Trigger all API calls based on the following criteria:
- Do a full entity pull upon receiving
status: FINISHED - Do partial data pulls based on mapping types marked as
OK/SUCCESS
- Do a full entity pull upon receiving
Webhooks
In all cases the below webhooks will be rate-limited to reduce noise - with the exception of a change of a refresh from PENDING → FINISHED which will always be sent at the point of completion.
{
"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",
"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>"
}
},{
"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>"
}
}
]
}
}
}Refreshing data
Lean will automatically refresh data on a periodic basis where ongoing access to an account is obtained to ensure that data is up-to-date.
Detecting Stale Data
All API responses for Data will now include a refreshed_at DateTime attribute in the response body
Triggering a manual Refresh
Manual refreshes may become chargeable in future
Data in the Store can be refreshed by calling /data/v2/refreshes if a consent is still available - a status of RECONNECT_REQUIRED will be returned along with a reconnect_id where no available session or consent is active.
POST 'https://api.leantech.me/data/v2/refreshes'
-h 'Authorization: Bearer <env.token>'
--data-raw '{
"entity_id": "ENTITY_ID"
}Successful
{
"timestamp": "2025-07-22T05:07:30.030450168Z",
"status": "Ok",
"message": "Data refresh was triggered successfully",
}
Reconnect entity is required (UAE)
{
"status": "RECONNECT_REQUIRED",
"message": "User input is required to reconnect with the bank",
"timestamp": "2025-07-24T08:46:17.848098201Z",
"reconnect_id": "RECONNECT_ID"
}
Consent is expired (KSA)
{
"timestamp": "2025-07-22T05:07:30.030450168Z",
"status": "CONSENT_EXPIRED",
"message": "Consent was suspended, revoked, or expired. New consent is required; please reconnect the entity.",
}Refresh automation: UAE
Reverse-engineered
- Data refresh is triggered upon a successful
.Reconnectin the LinkSDK. - Completion of a refresh is messaged with an
entity.data.refresh.updatedwebhook
Long-lived consent
- Data refresh is automatically triggered 4 times a day (provisional; in-line with off-session refreshes allowed by framework).
- On-session refreshes are triggered with a manual refresh.
- Completion of a refresh is messaged with an
entity.data.refresh.updatedwebhook when changes are detected.
Single-use consent(s)
- Lean will not trigger any refreshes outside of failure handling.
Refresh automation: KSA
Long-lived consent
- Data refresh is triggered 4 times a day (provisional; in-line with off-session refreshes allowed by framework).
- On-session refreshes are triggered with a manual refresh.
- Completion of a refresh is messaged with an
entity.data.refresh.updatedwebhook when changes are detected.
Single-use consent(s)
- Lean will not trigger any refreshes outside of failure handling.
- Manual refreshes can be triggered - however it likely makes more sense to run
.connect()each time.
Failure handling
What happens when a failure occurs?
In case there is any error in communication with bank we will retry the underlying request internally. We apply different retry strategies (different number of attempts and backoff) depending on error type. Retried requests will appear as still pending externally. There may occur additional delay caused by retries, but the design hides retry logic from clients.
How do I handle a failed request?
Retries are handled internally. If result is marked as failed this means that retries were already applied. Status may change only after next data refresh.
Updated about 1 month ago
