Skip to main content
Stateful Plaid Sandbox emulation: Link tokens and a hosted Link page, public token exchange, items, accounts and balances, Auth ACH numbers, transactions sync and get over deterministic sandbox data, institutions, the sandbox helpers, and webhooks signed with Plaid’s ES256 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

Plaid starts only when named in config or with --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 a POST 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.
  • 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, and access_token (update mode). It returns link_token (four hours), expiration, and hosted_link_url
  • POST /link/token/get — Get Link token: the token’s settings and its Link sessions, with each session’s success or exit
  • POST /item/public_token/exchange — Exchange public token: turns it into access_token (access-sandbox-...) and item_id
The hosted Link page at hosted_link_url (/hl/<link_token>) walks through what Plaid Link does:
  1. Pick an institution.
  2. Sign in with a sandbox login. The page lists the test logins.
  3. Answer MFA, if the login asks for it.
  4. Pick accounts. Auth only tokens offer checking and savings; account_filters apply.
On success the emulator makes an item and a public token (30 minutes, single use). With 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 status
  • POST /item/remove — Remove item: its token is refused afterwards
  • POST /item/webhook/update — Update webhook: sets the webhook and sends WEBHOOK_UPDATE_ACKNOWLEDGED
  • POST /item/access_token/invalidate — Rotate access token
  • POST /accounts/get — Retrieve accounts: works while the item has an error
  • POST /accounts/balance/get — Retrieve real time balances
  • POST /auth/get — Retrieve auth data: ACH numbers for checking, savings, and cash management accounts: account 111122223333<mask>, routing 011401533, wire routing 021000021
  • options.account_ids filters accounts on every route that takes it
The sandbox accounts match Plaid’s: checking 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 over days_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, options
  • POST /transactions/get — Get transactions: start_date, end_date, options
  • POST /transactions/refresh — Refresh transactions: accepted
Sync takes 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, when transactions_ready_delay_ms or transactions_ready: false held them back
  • POST /_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 sends LOGIN_REPAIRED

Institutions

  • POST /institutions/get_by_id — Get institution by ID
  • POST /institutions/search — Search institutions: query, products, country_codes
  • POST /institutions/get — Get institutions: count, offset, country_codes
The defaults are Plaid’s sandbox institutions: 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_username and override_password pick the login (MFA logins succeed; error logins fail with their code), and options.webhook and options.transactions.days_requested apply
  • POST /sandbox/item/fire_webhook — Fire webhook: webhook_code (and webhook_type for DEFAULT_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 extras PENDING_EXPIRATION, AUTOMATICALLY_VERIFIED, and VERIFICATION_EXPIRED. An item without a webhook is SANDBOX_WEBHOOK_INVALID
  • POST /sandbox/item/reset_login — Reset login: puts the item in ITEM_LOGIN_REQUIRED and sends ITEM ERROR
  • POST /sandbox/transactions/create — Create transactions: adds transactions and sends SYNC_UPDATES_AVAILABLE

Webhooks

Webhooks go to the item’s webhook (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:
Hash the raw body exactly as received: Plaid’s bodies are JSON with two space indentation.
  • POST /webhook_verification_key/get — Get webhook verification key: the public JWK for a key_id; an unknown id is INVALID_WEBHOOK_VERIFICATION_KEY_ID
Set webhook_signing_key in the seed to keep the same key across fresh starts.

Seed config

  • transactions_seed: changes the generated history
  • transactions_ready_delay_ms: how long new items stay PRODUCT_NOT_READY
  • access_token: derived when left out
Sandbox users may list their own 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.