Skip to main content
Sandbox is a full, live copy of the v3 API. Same endpoints, same webhooks, same routing logic, pointed at the aggregators’ own sandbox banks (Finicity FinBank, MX Bank) plus the FDE Ledgersync Bank, not synthetic fixtures. You link a real test bank, sign in with test credentials, and watch real accounts, transactions, and statements flow back exactly as they will in production.
Sandbox uses https://api-sandbox.ledgersyncappv2.com/v3 and an sk_test_... key. Live traffic never touches sandbox and vice-versa. See Authentication for how to get and send your key.

What sandbox gives you

  • The real routing engine. When you initiate a connection, v3 picks the source (Finicity, MX, or FDE) from the institution catalog, server-side, just like production. You never pass a source.
  • Real widgets. Finicity and MX open their own aggregator sandbox widgets. FDE opens the LedgerSync-hosted connect page. You still never see or transmit credentials.
  • Real webhooks. Every state transition publishes a real Kafka event and fires the matching webhook, HMAC-signed exactly like production.

Sandbox institution IDs

Pass one of these as institution_id on POST /v3/clients/{id}/connections. v3 resolves the source from the catalog row, you don’t pass a source field.

FinBank (Finicity)

ins_0a01a5430925d0b2Finicity’s sandbox bank. Widget renders on connect2.finicity.com (Finicity institutionId 8906).

MX Bank (MX)

ins_eaee71edf17fdacdMX’s sandbox bank (mxbank). Hands off to MX Connect’s sandbox on int-widgets.moneydesktop.com. Unlimited test aggregations.

Ledgersync Bank (FDE)

ins_a7397a8d0656e1b7FDE’s credential-based extractor. Returns a LedgerSync-hosted connect page (not an aggregator widget); accepts any login, offers one account.
Prove your integration in 6 calls: (1) mint an sk_test_ key, (2) register a webhook endpoint, (3) POST /v3/clients to create a test end-user, (4) POST /v3/clients/{id}/connections with institution_id=ins_0a01a5430925d0b2, open the widget, sign in customer1 / go, (5) confirm connection.active lands, then GET /v3/accounts (Checking 1111 + Savings 2222), (6) repeat step 4 with ins_eaee71edf17fdacd and mxuser / correct to prove the MX path.

The 60-second smoke test (FinBank)

FinBank is Finicity’s sandbox bank. It flips to active immediately, with no MFA, and auto-populates accounts, transactions, and statements. It is the fastest way to prove your integration works end-to-end.
1

Find FinBank

Search the catalog and pick the row named exactly FinBank — the sandbox has several FinBank * variants (see the table below) and only the plain one is the no-friction path.
Search
curl "https://api-sandbox.ledgersyncappv2.com/v3/institutions?q=FinBank" \
  -H "Authorization: Bearer sk_test_..."
Grab the id (ins_0a01a5430925d0b2) from the row whose name is FinBank.
2

Initiate the connection

Point it at a client you’ve created (see Connect a bank for creating clients).
Initiate
curl -X POST "https://api-sandbox.ledgersyncappv2.com/v3/clients/cli_.../connections" \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"institution_id":"ins_0a01a5430925d0b2"}'
You get 202 Accepted with an operation_id. Poll it until status is succeeded, then read result.connection.action.widget_url.
3

Open the widget and sign in

Open widget_url in a browser, pick FinBank, and sign in with the sandbox credentials:
FieldValue
Banking Useriddemo
Banking Passwordgo
No MFA, it links right away. (customer1 / go also works and returns known balances, see the table below.)
4

Watch it go active

The connection flips to active and connection.active fires on your webhook. The payload carries the canonical connection id (con_FINICITY_<bankAccountId>, e.g. con_FINICITY_41294). Store that, the placeholder con_<uuid> you saw during pending only works for the widget step.
5

Read the data

Accounts, transactions, and statements are already populated. Reads are Client-scoped, so pass client_id every time.
Accounts
curl "https://api-sandbox.ledgersyncappv2.com/v3/accounts?client_id=cli_...&connection_id=con_FINICITY_41294" \
  -H "Authorization: Bearer sk_test_..."
Transaction ids follow the same shape: txn_FINICITY_..., txn_MX_..., or txn_FDE_....
If you saw connection.active, a canonical con_FINICITY_... id, and non-empty accounts, your happy path is wired correctly. Now exercise the edge cases.

Finicity — FinBank credentials

Public sandbox logins published by Mastercard Open Banking. Sign in on the widget URL for ins_0a01a5430925d0b2. The username picks the scenario; the password is go unless noted.
ScenarioUsernamePasswordMFAExpected result
Happy path (verified)customer1gononeChecking 1111 ≈ 605.06/8txns+Savings2222605.06 / 8 txns + Savings 2222 ≈ 605.06 / 8 txns (Walmart fixtures).
Happy path (legacy demo)demogononeChecking + Savings, small synthetic history.
Wider shapecustomer2gononeChecking + Savings + Credit Card — multi-type serialization.
Text MFAtfa_textgotext prompt · answer goDrives connection.requires_actionconnection.active.
OTP text MFAcustomer3goOTP challenge · answer 999999Submit a wrong code to test the failure path.
Image MFAtfa_imagegoimage challenge · answer goExercises the image-MFA UI path.
Captchatfa_captchagocaptcha · answer any non-emptyAny non-empty value passes.
Invalid credentialscustomer1badn/aconnection.failed, last_error = invalid_credentials.
Locked institutionlockedgon/aconnection.failed, last_error = institution_locked.

FinBank Profiles A–H — account-shape coverage

For the FinBank Profiles - A / - B institutions, the password picks a profile (use the same value as the OAuth username too). Exercises every serializer path; investment coverage is intentionally thin, link a real brokerage for that.
Password (and OAuth username)Account shapes returned
profile_02Savings, IRA, 401k, Credit Card
profile_03Checking, Savings, Credit Card
profile_04Checking + Money Market + Mortgage
profile_05Checking, Savings, Credit Card, Auto Loan
profile_06Checking, Savings, Brokerage, Credit Card
profile_07Checking + Student Loan + Line of Credit
profile_08Multiple Checking accounts
profile_09Multiple Credit Cards

Other FinBank variants — edge-case banks

The catalog also surfaces several letter- and label-suffixed FinBank entries. Each one is Mastercard Open Banking’s dedicated sandbox bank for a specific edge case, pick a variant when you want to harden against that case, not for a smoke test. Search for each by name and initiate exactly as above.
Catalog namePurpose
FinBank Profiles - AAccount-shape coverage. The password picks a profile (profile_02profile_09) that returns a specific mix of Checking, Savings, IRA, 401k, Credit Card, Brokerage, Mortgage, Auto Loan, etc.
FinBank Profiles - BSecond profile-driven institution — same profile_* password mechanism as Profiles - A, distinct catalog row.
FinBank MMFA-flavoured variant (the “M” suffix). Use alongside the text / image / OTP MFA usernames to exercise the connection.requires_action → resume path.
FinBank OAlternate-aggregation variant. Use when you need a non-default Finicity routing for parsing or fixture-level regression coverage.
FinBank CCaptcha-flavoured variant. Pair with the tfa_captcha username to drive the captcha-MFA path in the widget.
FinBank PPersonal-banking variant. Use it to keep a consumer-shaped connection isolated from your business test connections.
FinBank OAuthOAuth sign-in flow. The widget redirects to a Finicity-hosted bank sign-in instead of taking credentials inline. The catalog row has oauth_supported = true.
FinBank DecertifiedDecertified-institution failure path. Drives connection.failed with a decertification-style last_error so you can verify your re-link CTA renders.
FinBank BillableBillable-statements variant. Verify your code handles the statements path end-to-end, including whatever your billing/usage accounting needs to count.

MX sandbox bank (mxbank)

MX is the alternate aggregator, it catches institutions Finicity misses, and v3 routes to it automatically when the catalog says so. Sign in on the widget URL for ins_eaee71edf17fdacd. The username is always mxuser; the password is the dial that picks the scenario. Ids come back source-tagged: con_MX_<id> (e.g. con_MX_1224), acc_MX_..., txn_MX_....
ScenarioUsernamePasswordResult
Happy path (verified)mxusercorrectChecking $495,565.18 / 49 txns, Savings, Credit Card, Loan, Mortgage, Investment.
Any non-keyword passwordmxuser<anything else>Connects normally. Use any string other than the keywords below.
Text MFAmxuserchallengeStandard text MFA · answer correct (any other answer fails).
Pick-one MFAmxuseroptionsOPTIONS selection challenge · answer correct.
Image MFAmxuserimageIMAGE selection challenge · answer correct.
Bad requestmxuserBAD_REQUEST400 missing fields. connection_status = FAILED.
Invalid credentialsmxuserUNAUTHORIZED401 invalid credentials. connection_status = DENIED.
Invalid loginmxuserINVALID401 login/password invalid. connection_status = DENIED.
Locked usermxuserLOCKED401 user is locked. connection_status = LOCKED.
Disabled usermxuserDISABLED401 locked-account variant. connection_status = DENIED.
Server errormxuserSERVER_ERROR500 institution-side error. connection_status = FAILED.
Service unavailablemxuserUNAVAILABLE503 service unavailable. connection_status = FAILED.
MX OAuth constraint: only one OAuth member is allowed per user for mx_bank_oauth / mxcu_oauth. Tear down the previous OAuth connection before re-linking.

FDE — Ledgersync Bank

FDE is LedgerSync’s proprietary, credential-based extraction path, used for banks neither aggregator covers. It connects through a LedgerSync-hosted page (not an aggregator widget). The sandbox Ledgersync Bank (ins_a7397a8d0656e1b7) accepts any username and password:
Initiate an FDE connection
curl -X POST "https://api-sandbox.ledgersyncappv2.com/v3/clients/cli_.../connections" \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{"institution_id":"ins_a7397a8d0656e1b7"}'
Open the returned widget_url and:
1

Enter any credentials

Type any username / password → Submit.
2

Select the account

An account-selection form appears (one account is offered) → Submit to keep it.
3

Watch it finish

FDE downloads the statements and fires connection.active. The connection id is con_FDE_... and transactions come back as txn_FDE_....
Because the bank is already fixed by institution_id, there is no bank-search step, and you never handle the credentials yourself.

Which webhook fires when

Every state transition publishes a real Kafka event and fires the matching webhook, HMAC-signed exactly like production.
EventWhen it firesHow to trigger it
connection.requires_actionAs soon as the widget URL is ready (right after connection.initiated).Payload carries data.action.widget_url — open it for the end-user.
connection.activeUser finishes the widget with valid credentials.Finicity customer1 / go or MX mxuser / correct. Accounts now visible via GET /v3/accounts.
connection.failedCredentials rejected, MFA failed, or upstream pipeline error.Finicity customer1 / bad or MX mxuser / UNAUTHORIZED. last_error carries a stable code.
connection.disconnectedEnd-user revokes access or LedgerSync tears down the link.DELETE /v3/connections/{id}, or Finicity locked / go.
These are the connection-state events you’ll drive in sandbox. The full catalog (connection.initiated, connection.capability_changed, account.refresh.completed, account.refresh.failed) and every payload shape is in Webhooks.

Testing your webhook handler

You don’t have to link a bank to prove your endpoint works. Fire a test event:
Test-fire a subscription
curl -X POST "https://api-sandbox.ledgersyncappv2.com/v3/webhooks/subscriptions/sub_.../test" \
  -H "Authorization: Bearer sk_test_..."
The test delivery is signed with your real signing secret and bypasses the event_types filter, so it reaches your handler even for events you didn’t subscribe to. It arrives as a webhook.test event, a safe way to confirm signature verification, your 2xx-within-30s response, and your event_id dedupe logic before real events flow.
Verify the signature the same way in sandbox as in production: constant-time HMAC-SHA256 over the raw body, and reject deliveries whose timestamp is more than 5 minutes old. See Webhooks for the full recipe.

Common-error recipes

Deliberately trigger each failure mode to harden your handler:
  • Invalid credentials → connection.failed — initiate FinBank, sign in customer1 / bad. Watch last_error = invalid_credentials land. (Or link FinBank Decertified for a decertification last_error.)
  • MFA wrong answer → connection.failed — initiate Finicity, sign in customer3 / go, submit any OTP other than 999999.
  • MX locked user — initiate MX, sign in mxuser / LOCKED. connection_status = LOCKED, connection.failed fires.
  • Upstream 503 → connection.failed — initiate MX, sign in mxuser / UNAVAILABLE. Simulates an institution-side 503; last_error indicates retryable.
  • requires_action that lingers — start a connection and don’t finish the widget. It stays requires_action (no auto timeout-to-failed today) until the user re-opens the still-valid widget_url or you re-initiate.
  • Refresh round-trip — link customer1 / go, POST /v3/connections/{id}/refresh, re-read GET /v3/accounts/{id}/transactions for the diff.
  • DisconnectDELETE /v3/connections/{id}. connection.disconnected fires; further reads return 404.
  • Error envelopes — call a read with a bogus client_id or a missing key to see the { "error": { "code", "message", "type", "doc_url", "trace_id" } } shape. Branch on code and quote X-LS-Trace-Id. See Errors.
Sandbox keys can’t hit live data and live keys can’t hit sandbox banks — see insufficient_scope if you mix them up. Finicity test customers are auto-pruned after ~30 days of inactivity; MX has no per-credential cooldown.

Connect a bank

The full initiate → widget → active flow, end to end.

Connection lifecycle

Every status a connection moves through and what each means.

Webhooks

Subscribe, verify signatures, dedupe, and handle retries.

Errors

The error envelope, the full code catalog, and how to branch on code.