API mocks
Lean Technologies
A mock compatible with the Lean open banking API (UAE and Saudi Arabia). Your app creates customers, connects their bank accounts, reads identity, accounts, balances and transactions, and takes payments from a bank account, as it does with Lean's sandbox, with Lean's response envelope and webhooks. The banks and their data are invented: no bank account is accessed and no money moves. Never enter real bank credentials on the mock's pages.
Testlie is not affiliated with, endorsed by or sponsored by Lean Technologies. The name is a trademark of its owner, used here only to say which API this mock is compatible with.
Connect
Create a Lean Technologies mock under API mocks → Banking. Use its base URL in place of https://sandbox.leantech.me and its credentials:
| Credential | Use |
|---|---|
| App id, client secret | POST /oauth2/token with grant_type=client_credentials (form-encoded) returns { access_token, token_type: "bearer", expires_in: 3599, scope }; send Authorization: Bearer <token> |
| App token | The legacy lean-app-token header, accepted instead of a bearer token |
| Payment destination id | The mock's one payment destination |
A missing or expired token answers 401 { "status": "UNAUTHORIZED" }.
The data API answers in Lean's envelope, { "status": "OK", "message", "timestamp", ... }, and every error uses it with an error status (CUSTOMER_NOT_FOUND, ENTITY_NOT_FOUND, INVALID_PARAMETERS, ...). The token, customer, entity, payment intent and payment destination endpoints return their objects without the envelope.
Customers
| Endpoint | |
|---|---|
POST /customers/v1 |
{ "app_user_id" } returns { customer_id, app_user_id }; an app user id used before answers 409 DUPLICATE_APP_USER_ID |
GET /customers/v1/{customer_id} |
One customer |
GET /customers/v1/app-user-id/{app_user_id} |
Look up by your own user id |
GET /customers/v1/{customer_id}/entities |
The customer's bank connections |
DELETE /customers/v1/{customer_id}/entities/{entity_id}?reason= |
Disconnect a bank; sends entity.deleted |
Connecting a bank (the Link page)
In production the LinkSDK runs in the customer's browser. The mock replaces it with a hosted page: send the browser to
<base URL>/testlie/link?method=CONNECT&customer_id=<id>&permissions=identity,accounts,balance,transactions&redirect_url=<your URL>The page lets the tester pick a bank and the result. It then redirects to redirect_url with what the LinkSDK callback would report:
| Result | Query parameters |
|---|---|
| Connected | method=CONNECT&status=SUCCESS&entity_id=…&bank_identifier=LEANMB1 and an entity.created webhook |
| Bank login fails | status=ERROR&secondary_status=BANK_LOGIN_FAILED |
| Cancelled | status=CANCELLED |
Banks: LEANMB1 (Lean Mock Bank) works normally; LEANMB2 connects but its data calls answer RECONNECT_REQUIRED, to test your reconnect flow; LEANMB3 works normally. /testlie/link is a Testlie extension, not a Lean endpoint.
Data API
POST /data/v1/identity, /data/v1/accounts, /data/v1/balance and /data/v1/transactions take { "entity_id", "account_id"? } and return { status: "OK", payload, results_id, message, timestamp, meta }. The data is invented but stable per entity: one AED current account with an IBAN, a balance and five transactions.
- A permission the customer did not grant answers
403 PERMISSION_NOT_GRANTED. - An unknown
account_idanswers404 ACCOUNT_NOT_FOUND. - With
"async": truethe call answersstatus: "PENDING"and aresults.readywebhook follows; fetch the data fromGET /data/v1/results/{results_id}.
Payments
| Endpoint | |
|---|---|
POST /payments/v1/intents |
{ amount, currency: "AED" | "SAR", customer_id, description?, payment_destination_id? } returns the intent with payment_intent_id |
GET /payments/v1/intents/{id} |
The intent with its payments |
GET /payments/v1/destinations |
The mock's payment destination |
To pay, send the browser to <base URL>/testlie/link?method=PAY&payment_intent_id=<id>&redirect_url=<your URL>. Approving creates a payment in PENDING_WITH_BANK (and a payment.created webhook) and redirects with status=SUCCESS&payment_id=…&payment_intent_id=…. After the mock's Asynchronous steps delay, the next read of the intent shows it ACCEPTED_BY_BANK with a bank_transaction_reference and sends payment.updated. Bank rejects the payment, or the everything is rejected scenario, creates a FAILED payment with the scenario's code (REJECTED_BY_BANK, INSUFFICIENT_FUNDS) and redirects with status=ERROR.
Webhooks
Events: entity.created, entity.deleted, results.ready, payment.created, payment.updated, posted to the mock's webhook URL in Lean's envelope:
{ "type": "payment.updated", "payload": { "id": "…", "intent_id": "…", "status": "ACCEPTED_BY_BANK", "amount": 150.25, "currency": "AED" }, "message": "A payment's status changed.", "timestamp": "2026-10-18T12:00:00.000Z", "event_id": "…" }The mock does not sign Lean webhooks. Treat them as notifications and read the object back from the API before acting on it.
Not supported
Lean's other products (account verification, payouts and disbursements, payment sources, the Saudi Arabia-specific flows, consent and reauthorization APIs) and mTLS are not mocked.