API mocks
Plaid-compatible banking API
A mock of Plaid's API with sandbox semantics. Your app creates link tokens, sends users through a Link flow, exchanges public tokens and reads accounts, balances, account numbers, identity and transactions, with Plaid's request and response shapes, error format and signed webhooks. Institutions, accounts, people and transactions are invented.
Testlie is not affiliated with, endorsed or sponsored by Plaid Inc. "Plaid" is used only to describe API compatibility.
Connect
Create a Plaid-compatible mock under API mocks → Banking. Use its base URL in place of https://sandbox.plaid.com, with its client_id and secret, sent as the client_id and secret body fields or the PLAID-CLIENT-ID and PLAID-SECRET headers (what the official SDKs send).
import { Configuration, PlaidApi } from "plaid";
const plaid = new PlaidApi(new Configuration({
basePath: "https://testlie.com/mock/<id>",
baseOptions: { headers: { "PLAID-CLIENT-ID": process.env.PLAID_CLIENT_ID, "PLAID-SECRET": process.env.PLAID_SECRET } },
}));In Python: plaid.Configuration(host="https://testlie.com/mock/<id>", api_key={"clientId": ..., "secret": ...}).
Tokens carry a testlie marker (link-sandbox-testlie-…, public-sandbox-testlie-…, access-sandbox-testlie-…) so they are never mistaken for real Plaid tokens.
Errors
Errors use Plaid's shape and HTTP status:
{ "error_type": "INVALID_INPUT", "error_code": "INVALID_API_KEYS", "error_message": "invalid client_id or secret provided",
"display_message": null, "request_id": "…", "causes": [], "status": 400, "documentation_url": "…", "suggested_action": null }Codes you may see: MISSING_FIELDS, INVALID_FIELD, INVALID_PRODUCT, INVALID_API_KEYS, INVALID_ACCESS_TOKEN, INVALID_PUBLIC_TOKEN, INVALID_LINK_TOKEN, INVALID_ACCOUNT_ID, INVALID_INSTITUTION, ITEM_LOGIN_REQUIRED, ITEM_NOT_FOUND, RATE_LIMIT (429) and INTERNAL_SERVER_ERROR (500, the API failure scenario).
Endpoints
| Endpoint | |
|---|---|
/link/token/create |
client_name, language, country_codes, user.client_user_id and products (or access_token for update mode); webhook; hosted_link.completion_redirect_uri or redirect_uri. Returns link_token, expiration (4 hours) and hosted_link_url |
/link/token/get |
Includes link_sessions[].results.item_add_results[].public_token once the flow finished |
/item/public_token/exchange |
Public tokens are single use and expire after 30 minutes |
/accounts/get, /accounts/balance/get |
options.account_ids filters |
/auth/get |
ACH numbers for depository accounts (routing 110000000) |
/identity/get |
Accounts with owners |
/transactions/sync |
cursor, count (1-500); added, modified, removed, next_cursor, has_more |
/item/get, /item/remove, /item/webhook/update |
|
/institutions/get_by_id |
|
/sandbox/public_token/create |
institution_id, initial_products, options.webhook; creates an item without Link |
/sandbox/item/fire_webhook |
webhook_code: SYNC_UPDATES_AVAILABLE, DEFAULT_UPDATE, NEW_ACCOUNTS_AVAILABLE, ERROR, LOGIN_REPAIRED, PENDING_EXPIRATION, USER_PERMISSION_REVOKED |
/sandbox/item/reset_login |
Puts the item into ITEM_LOGIN_REQUIRED |
/webhook_verification_key/get |
The public key for Plaid-Verification |
Institutions use the familiar sandbox ids with neutral names: ins_109508, ins_109509, ins_109510, ins_109511, ins_109512. Every item has a checking (mask 0000), a savings (1111) and a credit card account (3333); Link lets the user share a subset.
Transactions are deterministic: an item always has the same 24 transactions over the 45 days before it was created. fire_webhook with SYNC_UPDATES_AVAILABLE or DEFAULT_UPDATE adds 3 new ones, returned by the next /transactions/sync from your last cursor. Nothing is ever modified or removed.
While an item needs a new login (reset_login, or fire_webhook with ERROR), /accounts/balance/get, /auth/get, /identity/get and /transactions/sync answer ITEM_LOGIN_REQUIRED; /accounts/get and /item/get still work. Repair it with Link in update mode (a link token created with the item's access_token).
Not implemented: assets, investments, liabilities, income, payment initiation, transfer, processor tokens and OAuth institutions.
The hosted Link flow
Instead of the Link SDK, send the user's browser to the link token's hosted_link_url. The page walks through institution, credentials, MFA when the password asks for it, and account selection.
It only accepts Plaid's published sandbox test credentials and shows a full-width "Testlie test environment" banner. Never enter real bank credentials: anything other than the test credentials is refused, and nothing typed on the page is stored, logged or forwarded.
| Username | Password | Result |
|---|---|---|
user_good |
pass_good |
Success |
user_good |
mfa_device |
Device code; the code is 1234 |
user_good |
mfa_questions_<n>_<m> |
n rounds of m questions; answers are answer_<round>_<question> |
user_good |
mfa_selections |
Choose Yes |
user_good |
error_<CODE> |
Shows that institution error (ITEM_LOCKED, INSTITUTION_DOWN, NO_ACCOUNTS, ...) and stays on the login step |
When the flow ends the browser goes to hosted_link.completion_redirect_uri (or redirect_uri) with status=success or status=exited, link_session_id and, on success, public_token (a Testlie convenience; the public token is also in /link/token/get and the SESSION_FINISHED webhook). Without a redirect URI the page shows the public token. Redirect URIs must be absolute http(s) URLs; javascript:, data: and similar are refused when the link token is created.
Link pages expire with their link token (4 hours) and at the latest 24 hours after they were created. In update mode the flow ends after the login: the item's error clears and ITEM LOGIN_REPAIRED is sent.
Webhooks
Webhooks go to the item's webhook (from /link/token/create, /sandbox/public_token/create or /item/webhook/update), or else to the mock's webhook URL. The mock sends:
| Type | Code | When |
|---|---|---|
TRANSACTIONS |
SYNC_UPDATES_AVAILABLE |
An item with the transactions product is created; fire_webhook |
TRANSACTIONS |
DEFAULT_UPDATE |
fire_webhook |
ITEM |
ERROR (with ITEM_LOGIN_REQUIRED) |
reset_login; fire_webhook with ERROR |
ITEM |
LOGIN_REPAIRED |
Update mode Link; fire_webhook |
ITEM |
NEW_ACCOUNTS_AVAILABLE, PENDING_EXPIRATION, USER_PERMISSION_REVOKED, WEBHOOK_UPDATE_ACKNOWLEDGED |
fire_webhook, /item/webhook/update |
LINK |
SESSION_FINISHED |
The hosted Link flow ends (status SUCCESS or EXITED, public_tokens) |
Each webhook carries a Plaid-Verification header: a JWT signed with ES256 whose header has the key id (kid) and whose claims are iat and request_body_sha256 (the SHA-256 of the body, hex). Verify it like Plaid documents: fetch the key with /webhook_verification_key/get and { "key_id": kid }, check the ES256 signature with that JWK, check that iat is recent and compare the body's SHA-256. Each mock has its own key pair; rotating the mock's credentials replaces it.