Plaid-Verification JWT. No real bank is ever contacted. The tests drive it with the official plaid Node SDK (48.0.0) and verify webhooks with jose.
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000. A fresh emulator accepts the default credentials below and serves Plaid’s five sandbox institutions; items come from Link, the sandbox helpers, or the seed.
Point the SDK at the emulator with basePath:
URL mapping
Auth
Every endpoint is aPOST with a JSON body. Credentials go in the body (client_id, secret) or in the PLAID-CLIENT-ID and PLAID-SECRET headers. A fresh emulator accepts client id 6650f1c0d1e2f3a4b5c6d7e8 and secret baselinesandboxsecret000000000; client_id and secret in the seed replace them. Missing credentials are 400 MISSING_FIELDS, wrong ones 400 INVALID_API_KEYS.
Errors
Errors use Plaid’s error object:{ error_type, error_code, error_code_reason, error_message, display_message, request_id, causes, status, documentation_url, suggested_action }. Unknown fields are UNKNOWN_FIELDS, missing required fields MISSING_FIELDS, and bad values INVALID_FIELD. An unknown access token is INVALID_ACCESS_TOKEN; an item that needs a new login is ITEM_LOGIN_REQUIRED; transactions that are not ready yet are PRODUCT_NOT_READY. Every response carries a request_id.
Link
POST /link/token/create— Create Link token:user.client_user_id,client_name,products,country_codes,language,webhook,redirect_uri,account_filters,transactions.days_requested,hosted_link, andaccess_token(update mode). It returnslink_token(four hours),expiration, andhosted_link_urlPOST /link/token/get— Get Link token: the token’s settings and its Link sessions, with each session’s success or exitPOST /item/public_token/exchange— Exchange public token: turns it intoaccess_token(access-sandbox-...) anditem_id
hosted_link_url (/hl/<link_token>) walks through what Plaid Link does:
- Pick an institution.
- Sign in with a sandbox login. The page lists the test logins.
- Answer MFA, if the login asks for it.
- Pick accounts. Auth only tokens offer checking and savings;
account_filtersapply.
hosted_link.completion_redirect_uri, the page redirects there with public_token, link_session_id, and status=success added. Otherwise it shows the token and posts { source: "baseline-plaid-link", action: "success", public_token, metadata } to window.opener or the parent frame. Exiting posts action: "exit" with the error and status, or redirects with status=exited. A Link token made with access_token repairs that item in update mode.
Sandbox logins
Logins seeded under
sandbox_users are checked first.
Items and accounts
POST /item/get— Retrieve item: the item and its statusPOST /item/remove— Remove item: its token is refused afterwardsPOST /item/webhook/update— Update webhook: sets the webhook and sendsWEBHOOK_UPDATE_ACKNOWLEDGEDPOST /item/access_token/invalidate— Rotate access tokenPOST /accounts/get— Retrieve accounts: works while the item has an errorPOST /accounts/balance/get— Retrieve real time balancesPOST /auth/get— Retrieve auth data: ACH numbers for checking, savings, and cash management accounts: account111122223333<mask>, routing011401533, wire routing021000021options.account_idsfilters accounts on every route that takes it
0000, savings 1111, CD 2222, credit card 3333, money market 4444, IRA 5555, 401k 6666, student loan 7777, and mortgage 8888, with Plaid’s documented balances.
Transactions
Each item gets a deterministic history overdays_requested (90 by default): payroll, rent, card payments, interest, savings transfers, and everyday merchants, with Plaid’s personal finance categories. The same seed and item give the same transactions on every run. Card purchases from the last two days are pending.
POST /transactions/sync— Sync transactions:cursor,count,optionsPOST /transactions/get— Get transactions:start_date,end_date,optionsPOST /transactions/refresh— Refresh transactions: accepted
cursor, count (1 to 500, default 100), and options (account_id, include_original_description). It returns added, modified, removed, next_cursor, has_more, accounts, and transactions_update_status. Start with no cursor, page until has_more is false, and keep the last next_cursor for the next sync. Changes between pages fail the next page with TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION; restart from the cursor that began the loop. While transactions are not ready, sync returns an empty page with next_cursor: "" and NOT_READY.
Get takes start_date, end_date, and options (count up to 500, offset, account_ids). It returns transactions newest first, total_transactions, accounts, and item.
New transactions come from POST /sandbox/transactions/create (up to 10). Modify and remove them with the emulator controls:
modify takes amount, name, merchant_name, date, or pending: false. Posting a pending transaction removes it and adds a posted one with pending_transaction_id, as Plaid does.
POST /_plaid/items/<item_id>/transactions/ready— Make transactions ready: now, whentransactions_ready_delay_msortransactions_ready: falseheld them backPOST /_plaid/items/<item_id>/error— Set item error:{ "error_code": "ITEM_LOGIN_REQUIRED" }puts an item into an error state and sends the webhook;{ "error_code": null }clears it and sendsLOGIN_REPAIRED
Institutions
POST /institutions/get_by_id— Get institution by IDPOST /institutions/search— Search institutions:query,products,country_codesPOST /institutions/get— Get institutions:count,offset,country_codes
ins_109508 First Platypus Bank, ins_109509 First Gingham Credit Union, ins_109510 Tattersall Federal Credit Union, ins_109511 Tartan Bank, and ins_109512 Houndstooth Bank.
Sandbox helpers
POST /sandbox/public_token/create— Create public token: makes an item without Link.options.override_usernameandoverride_passwordpick the login (MFA logins succeed; error logins fail with their code), andoptions.webhookandoptions.transactions.days_requestedapplyPOST /sandbox/item/fire_webhook— Fire webhook:webhook_code(andwebhook_typeforDEFAULT_UPDATE):SYNC_UPDATES_AVAILABLE,DEFAULT_UPDATE,RECURRING_TRANSACTIONS_UPDATE,NEW_ACCOUNTS_AVAILABLE,USER_PERMISSION_REVOKED,USER_ACCOUNT_REVOKED,PENDING_DISCONNECT,LOGIN_REPAIRED,ERROR,SMS_MICRODEPOSITS_VERIFICATION, and the Baseline extrasPENDING_EXPIRATION,AUTOMATICALLY_VERIFIED, andVERIFICATION_EXPIRED. An item without a webhook isSANDBOX_WEBHOOK_INVALIDPOST /sandbox/item/reset_login— Reset login: puts the item inITEM_LOGIN_REQUIREDand sendsITEMERRORPOST /sandbox/transactions/create— Create transactions: adds transactions and sendsSYNC_UPDATES_AVAILABLE
Webhooks
Webhooks go to the item’swebhook (from the Link token, the sandbox helper, /item/webhook/update, or the seed). The emulator sends TRANSACTIONS SYNC_UPDATES_AVAILABLE (plus INITIAL_UPDATE, HISTORICAL_UPDATE, DEFAULT_UPDATE, and TRANSACTIONS_REMOVED until the item first calls sync), ITEM ERROR, PENDING_EXPIRATION, USER_PERMISSION_REVOKED, LOGIN_REPAIRED, and WEBHOOK_UPDATE_ACKNOWLEDGED, AUTH events, and LINK ITEM_ADD_RESULT and SESSION_FINISHED for hosted Link. Failed posts are retried after 1, 4, and 16 seconds.
Each post carries a Plaid-Verification header: an ES256 JWT with iat and request_body_sha256. Verify it the way Plaid’s docs describe:
POST /webhook_verification_key/get— Get webhook verification key: the public JWK for akey_id; an unknown id isINVALID_WEBHOOK_VERIFICATION_KEY_ID
webhook_signing_key in the seed to keep the same key across fresh starts.
Seed config
transactions_seed: changes the generated historytransactions_ready_delay_ms: how long new items stay PRODUCT_NOT_READYaccess_token: derived when left out
accounts (name, mask, type, subtype, available, current, limit); they get Plaid’s sandbox accounts otherwise. transactions_ready: false on an item holds its transactions until POST /_plaid/items/<item_id>/transactions/ready. Seeding again does not duplicate entries.
Inspector
GET /_inspector has tabs for items with their accounts, transactions per item, Link tokens and sessions, webhook deliveries with their bodies, and setup (credentials and sandbox logins).
Limits
- Only Transactions, Auth, and Balance have endpoints. Link tokens accept the other product names, but Identity, Investments, Liabilities, Assets, Transfer, and Income routes are not served (404
NOT_FOUND). - No OAuth institutions, Plaid Link SDK script (
link-initialize.js), or embedded Link; use the hosted Link page. - No recurring transactions endpoint, enrichment, or categorization beyond the generated personal finance categories.
- Webhook retries run three times over about 20 seconds, where Plaid retries for longer.
- Plaid publishes no licensed OpenAPI spec, so routes are checked against the SDK, not a vendored spec.
Conformance
See CONFORMANCE.md for every provider.