> ## Documentation Index
> Fetch the complete documentation index at: https://docs.straddle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox testing for Bridge bank connections

> Test every Bridge widget bank connection path in the Straddle sandbox, including OAuth redirects, credential logins with MFA challenges, and manual entry, using named test institutions and published test credentials.

Bank connection flows are the hardest part of a payments integration to test. Your application has to handle OAuth redirects that leave and re-enter your page, credential logins with several kinds of MFA challenges, connection errors, and users who skip bank login entirely and type in their account details. None of that is practical to test against a real bank.

The Straddle sandbox solves this with a family of test institutions called FinBank. Each one exercises a different connection path with published credentials, so you can walk every branch of your integration, including the failure branches, without real bank credentials.

> [!NOTE]
>
> This guide covers the bank connection paths inside the [Bridge widget](https://docs.straddle.com/guides/bridge/widget.md). To simulate charge, payout, and return outcomes after a connection is made, see [sandbox simulations for Pay by Bank](https://docs.straddle.com/guides/resources/sandbox-paybybank.md).

## Before you start

Make sure you have:

* A sandbox account and API key
* A [customer](https://docs.straddle.com/guides/identity/customers.md) created in the sandbox
* The [Bridge widget](https://docs.straddle.com/guides/bridge/widget.md) on your page, with `mode="sandbox"` on the `<straddle-bridge>` element
* A Bridge token from [`POST /v1/bridge/sessions`](https://docs.straddle.com/guides/bridge/widget#create-a-bridge-token), sent to `https://sandbox.straddle.com`

### Set a redirect URL for OAuth

OAuth connections send the user to a bank-hosted page and back again, so the widget needs somewhere to return to. Pass `redirect_url` when you create the bridge token:

```json theme={null}
{
  "customer_id": "8cfe1f5d-1a3f-4e7a-9e1b-6f2c4a1d7e33",
  "redirect_url": "https://example.com/bank-return"
}
```

The URL must be an absolute `https` URL. You don't need to pre-register it with Straddle or with the bank.

`redirect_url` is required. If you omit it, `POST /v1/bridge/sessions` returns a validation error and Straddle doesn't create the token.

## Choose a test institution

Search for the institution by name in the widget's bank search, the same way a real user would.

| Test institution | Connection path | Sign-in |
| - | - | - |
| **Finbank OAuth** | OAuth redirect to a bank-hosted login | Test credentials below |
| **FinBank Profiles - A** | Credential login inside the widget, MFA challenges | Test credentials below |

These two are the institutions Straddle tests against, and every scenario in this guide is written for them. Bank search also returns other FinBank variants, which exist to test products outside the scope of a paykey. If you select one and the connection fails with `Institution is not available for connection.`, that institution does not expose the account ownership and balance data Straddle needs, and the fix is to pick one of the two above.

## Test credentials

FinBank has two separate families of test credentials, and mixing them up is the second most common cause of a failed sandbox test.

**Authentication profiles** test sign-in behavior: success, errors, and MFA challenges. The password is always `go`, except when you are triggering a specific error code, where the password *is* the code. These work at either supported institution.

| Username | Password | What happens |
| - | - | - |
| `demo` | `go` | Successful sign-in |
| `demo` | `123` | Sign-in fails with an institution error |
| `demo` | `imagechoice` or `imagechoice2` | Image-selection challenge, any answer accepted |
| `invalid_user` | `go` | Error 103, invalid credentials |
| `tfa_text` | `go` | Text-message code challenge |
| `tfa_image` | `go` | Image captcha, any answer accepted |
| `tfa_choice` | `go` | Multiple-choice question |
| `tfa_multi` | `go` | Image-selection challenge, any answer accepted |
| `discovertimeout` | `go` | Account discovery doesn't finish. After 90 seconds, the widget shows "We didn't hear back from FinBank" with three options: start again, choose another bank, or enter details manually |

Other password values for `demo` fail as ordinary invalid credentials rather than producing an institution error.

**Account profiles** test what the connection returns: account types, owner names, and balances. Here the username and password are the same value, the profile id itself.

| Username and password | Account holder | Accounts |
| - | - | - |
| `profile_02` | Homer Loanseeker | Savings |
| `profile_03` | Ann Sea | Checking, investment, 401k, Roth, savings (joint owners) |
| `profile_04` | Alice Firstimer | Checking, 403b, 529, rollover, mortgage |
| `profile_05` | Lorraine Purchaser | Checking, investment, stocks, UGMA, UTMA (joint owners) |
| `profile_08` | Andy Miller | Checking, savings, money market, 401a, line of credit |

The account holder name is what Straddle matches against your customer's name, so these are the profiles to use when you are testing verification outcomes rather than sign-in behavior.

## Test the OAuth flow

> [!WARNING]
>
> Finbank OAuth requires registration before its profiles will accept a sign-in. If OAuth sign-ins fail for every profile, contact Straddle support before debugging your integration.

1. Create a bridge token with a `redirect_url`.
2. In the widget, search for **Finbank OAuth** and select it.
3. Complete the bank-hosted login with an account profile, for example `profile_02` / `profile_02`.
4. Approve the connection and return to the widget.

When the redirect returns, the widget resumes at account selection without restarting the session. After the user selects an account, Straddle creates a paykey and fires a `paykey.created.v1` webhook.

## Test the credential login flow

Search for **FinBank Profiles - A** and sign in with one of the profiles above. The widget shows the login fields inside Bridge, but the username and password never reach Straddle. You are testing your handling of the form, the challenges, and the error states.

Use an authentication profile when you are testing the form itself, and an account profile when you want a particular set of accounts or a particular account holder name.

### MFA challenges

Each `tfa_` profile presents a different challenge type. Two of them recognize a special answer that changes what happens next; the rest accept anything.

| Profile | Challenge | Answer to continue | Special answer |
| - | - | - | - |
| `tfa_text` | Text-message code | Any value other than `mfa` | `mfa` returns another round, repeatable |
| `tfa_choice` | Multiple choice | Any option | `fail` ends the connection with error 187 |
| `tfa_image` | Image captcha | Any answer | None |
| `tfa_multi` | Image selection | Any selection | None |
| `demo` / `imagechoice` | Image selection, alternate style | Any selection | None |

### Connection errors

| Sign-in | Error code | What the user sees |
| - | - | - |
| `invalid_user` / `go` | 103 | A message that the username or password did not match |
| `demo` / `123` | 123 | A technical problem at the institution, with a suggestion to try again or pick another bank |
| `tfa_choice`, answered `fail` | 187 | Incomplete authorization |

On any error, and on a canceled login, the widget shows a retry option and no paykey is created. Your application should not receive a paykey callback in these cases.

## Test manual entry

Choose manual entry in the widget and provide a routing and account number. The paykey is created synchronously with source `bank_account`.

The routing number must be nine digits and pass the ABA checksum, so an arbitrary number is rejected. Use a real routing number for testing, for example `021000021` (JPMorgan Chase).

The account number must be 8 to 12 digits. With routing number `021000021` and an account number such as `5550193847`, Straddle creates the paykey with status `active`. Don't use `123456789`: the sandbox treats it as a rejection number, even though it's in that range.

Manual paykeys carry no balance. Verification still runs and still weighs the account holder against your customer, but because the bank returns no owner data on this path, the result carries no names on the account, no matched name, and no correlation score. You get the decision without the evidence behind it.

There is no micro-deposit verification step. A valid manual entry creates the paykey immediately, with nothing for the user to confirm later.

## Test name matching

Straddle compares the account holder name the bank returns against your customer's name, and that comparison decides whether the paykey activates or goes to review. A paykey from a bank login goes to review only when the names don't match, so the way to test review is to connect a profile whose holder name differs from your customer's name.

These profiles return a known holder name:

| Institution | Username and password | Holder name the bank returns |
| - | - | - |
| **FinBank Profiles - A** or **Finbank OAuth** | `profile_02` | Homer Loanseeker |
| **FinBank** | `profile_2513` | John Doe |

Use the customer's name to pick the outcome:

| Customer name, connecting with `profile_02` | Result |
| - | - |
| Homer Loanseeker | `active`: the names match |
| A different given name with the same surname, such as Marge Loanseeker | `active`: a shared surname is close enough to match |
| A different name, such as Homer Smith or Jess Sea | `review`: the names don't match |

### Trigger the review scenario

To put a bank-login paykey into review, follow these steps:

1. Create a customer whose name doesn't match the profile's holder name, for example `"name": "Homer Smith"`.

2. Create a bridge token for that customer. Leave `sandbox_outcome` unset, or set it to `standard`, so that the name comparison decides the outcome.

3. In the widget, search for **FinBank Profiles - A** and sign in with `profile_02` / `profile_02`.

4. Select the savings account and select **Link account**.

   The widget ends on **Account details received** instead of **Account linked**, and your application receives the paykey with status `review`, reason `require_review`, and source `watchtower`.

5. Create a charge against the paykey.

   The charge goes to `on_hold` with reason `risk_review` until the review is decided.

6. Retrieve the review with [`GET /v1/paykeys/{id}/review`](https://docs.straddle.com/api-reference/paykeys/review).

   The `verification_details.breakdown.name_match` object shows `names_on_account`, `matched_name`, `customer_name`, and `correlation_score`, so you can test how your application presents the evidence behind a review.

7. Decide the review with [`PATCH /v1/paykeys/{id}/review`](https://docs.straddle.com/api-reference/paykeys/patch_review), using `{"status": "active"}` to approve or `{"status": "rejected"}` to reject.

   Approving activates the paykey and releases the held charge without a separate release call. Both decisions fire a `paykey.event.v1` webhook.

The review data comes from the bank's real holder names. To see how your application handles a review with no matching evidence at all, connect the same profile with a customer whose name shares nothing with it, such as Jess Sea.

## Force a specific paykey outcome

To skip the name comparison and force a paykey to `active` or `rejected`, set `sandbox_outcome` in the `config` object.

For the widget paths, set `sandbox_outcome` on the bridge token. Every paykey created in that widget session inherits it:

```json theme={null}
{
  "customer_id": "8cfe1f5d-1a3f-4e7a-9e1b-6f2c4a1d7e33",
  "redirect_url": "https://example.com/bank-return",
  "config": { "sandbox_outcome": "rejected" }
}
```

For manual entry, set `sandbox_outcome` in the [`POST /v1/bridge/bank_account`](https://docs.straddle.com/api-reference/bridge/bank-account) request instead.

| `sandbox_outcome` | Bridge token (widget bank login) | `POST /v1/bridge/bank_account` (manual entry) |
| - | - | - |
| `standard` | Name matching decides the outcome | Bank account validation decides the outcome |
| `active` | `active` | `active` |
| `rejected` | `rejected` | `rejected` |
| `review` | Not supported. Treated as `standard` | `review` |

Bridge tokens don't support `review`, because a bank-login paykey goes to review only on a name mismatch. To test review on the widget paths, [trigger the review scenario](#trigger-the-review-scenario) with a mismatched customer name instead. That way the review carries the bank's real holder names.

A forced outcome skips the name comparison, so the paykey's review data shows placeholder names, not the names the bank returned.

Outside the sandbox, the API rejects `sandbox_outcome` as invalid.

## Test account balances

FinBank Profiles - A returns a fixed balance per profile, so you can test [balance checks](https://docs.straddle.com/guides/payments/charges.md#balance-verification) against a known figure rather than whatever a test account happens to hold.

| Username and password | Balance on the connected account |
| - | - |
| `profile_1151` | \$4.90 |
| `profile_1153` | -\$33.97 |
| `profile_1154` | \$3,977.79 |
| `profile_1155` | \$915.64 |

Use `profile_1151` to test a charge that exceeds the available balance, and `profile_1153` for an account already overdrawn.

To test a balance the bank refuses to return, sign in as `tfa_multi` / `go` and connect the savings or checking account. Balance retrieval fails with error 185. Your integration should treat the missing balance as unknown rather than as zero.

Balances are cached for up to an hour. Refreshing a paykey inside that window returns the same figure and makes no new call to the bank, so a balance that looks stale during testing is usually the cache rather than a bug.

Manual entry paykeys have no balance at all. A charge against one with [balance checks](https://docs.straddle.com/guides/payments/charges.md#balance-verification) set to `required` ends with status `failed` and reason `insufficient_funds`, because Straddle can't retrieve a balance for the account.

## What to verify

However the bank was connected, verify the same things on your side:

* **Source.** Paykeys created through the widget's bank login paths have source `straddle`. Manual entry paykeys have source `bank_account`.
* **Webhooks.** A successful connection fires `paykey.created.v1`. Status changes fire their own events, so your handlers should process the full [paykey lifecycle](https://docs.straddle.com/guides/bridge/paykeys.md).
* **Name matching.** When the bank-returned account owner matches the customer's name, the paykey activates. A mismatch places it in `review`, and Straddle holds payments against it until the review is decided. Approving the review activates the paykey and releases held charges. Rejecting it moves the paykey to `rejected` and fires a webhook. On the bank login paths the review shows the names on the account and a correlation score; on manual entry it shows the decision alone.
* **[Balance checks](https://docs.straddle.com/guides/payments/charges.md#balance-verification).** Test the low-balance and retrieval-failure accounts with balance checks set to `enabled` and to `required`.


## Related topics

- [Sandbox simulation testing for Pay by Bank](https://docs.straddle.com/guides/resources/sandbox-paybybank.md)
- [Bridge widget for connecting bank accounts](https://docs.straddle.com/guides/bridge/widget.md)
- [2026 changelog and product updates](https://docs.straddle.com/changelog/updates/2026.md)
- [Create a paykey from a Mastercard token](https://docs.straddle.com/guides/bridge/mastercard-consent.md)
- [Bridge API overview](https://docs.straddle.com/guides/bridge/overview.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.