Testing Bank Linking

Introduction

This guide walks bank linking end to end in the sandbox environment: create a link, complete it against a provider test bank, read the created resources, pull bank transactions, then simulate a card transaction and watch it reconcile against a bank transaction. Every request runs against https://api.sandbox.astrada.co with a bearer token from Get a token. The flow is identical to production; only the bank behind the hosted window is fake.

You can complete the link two ways: open the hostedLink from the API response directly (steps 3 and beyond), or embed the Unified Enrollment SDK from the sandbox host (step 4). Both end in the same webhooks and resources.

1. Create a subaccount and register a webhook

Create a fresh subaccount for the test run, so cleanup is trivial and events are easy to attribute:

POST /subaccounts HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "name": "bank-linking-test",
  "configurations": {
    "VISA": { "countries": "*" },
    "MASTERCARD": { "countries": "*" }
  }
}
curl -X POST https://api.sandbox.astrada.co/subaccounts \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "bank-linking-test",
    "configurations": {
      "VISA": { "countries": "*" },
      "MASTERCARD": { "countries": "*" }
    }
  }'
{
  "name": "bank-linking-test",
  "configurations": {
    "VISA": { "countries": "*" },
    "MASTERCARD": { "countries": "*" }
  }
}

Response (201 Created): the subaccount; keep its id as {subaccountId} for everything below.

Then register a webhook for the bank events. A webhook.site URL works well as a throwaway receiver:

curl -X POST https://api.sandbox.astrada.co/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subaccountId": "{subaccountId}",
    "url": "https://webhook.site/your-unique-url",
    "description": "bank linking test",
    "eventTypes": [
      "banklink.completed",
      "bankaccount.state_changed",
      "cardsubscription.created",
      "banktransaction.created",
      "transaction.created",
      "transaction.updated",
      "transaction.match.created"
    ]
  }'

Response (201 Created): the webhook with its signing secret. What each event means is covered in Bank Linking Webhooks & Events.

2. Choose the bank rail

Bank links are created against a provider, resolved from provider and countryCode: plaid connects US and Canadian banks (USA, CAN), and mastercard is Mastercard Open Finance, which connects US banks (USA, through Finicity) and EU banks (DEU, FRA, NLD, ESP, BEL, PRT). You can pass the provider per link in step 3, or a subaccount default can be set that the SDK path also uses. Setting the default requires the accounts:write scope, which is held by Astrada-managed admin clients rather than integration clients, so ask Astrada to configure it; in this walkthrough, pass the provider per link.

curl -X PATCH "https://api.sandbox.astrada.co/subaccounts/{subaccountId}/bank-linking-policy" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "defaultProvider": "plaid",
    "defaultCountryCode": "USA",
    "accountTypes": ["credit"]
  }'

Response (200 OK): the effective policy. accountTypes: ["credit"] keeps the hosted window focused on credit-card accounts, which is also what card minting works from; the system default is credit-only.

3. Create a bank link with the API

POST /bank-links HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "id": "sandbox-walkthrough-1",
  "subaccountId": "{subaccountId}",
  "provider": "plaid",
  "countryCode": "USA"
}
curl -X POST https://api.sandbox.astrada.co/bank-links \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "sandbox-walkthrough-1",
    "subaccountId": "{subaccountId}",
    "provider": "plaid",
    "countryCode": "USA"
  }'
{
  "id": "sandbox-walkthrough-1",
  "subaccountId": "{subaccountId}",
  "provider": "plaid",
  "countryCode": "USA"
}

Response (201 Created):

{
  "id": "sandbox-walkthrough-1",
  "subaccountId": "...",
  "state": "pending",
  "hostedLink": "https://secure.plaid.com/hl/...",
  "linkExpiresAt": "2026-08-28T16:00:00Z",
  "createdAt": "2026-08-28T12:00:00Z"
}

Open the hostedLink in a browser and complete it against the provider's test bank.

Plaid ("provider": "plaid"): in the hosted window, search for First Platypus Bank. Sign in with username user_custom and, as the password, a JSON object describing the accounts you want. This one creates a credit-card account with a transaction dated today, which step 7 will reconcile against:

{
  "override_accounts": [
    {
      "type": "credit",
      "subtype": "credit card",
      "mask": "4545",
      "starting_balance": 500,
      "transactions": [
        {
          "date_transacted": "2026-08-28",
          "date_posted": "2026-08-28",
          "amount": 4.50,
          "description": "BLUE BOTTLE COFFEE",
          "currency": "USD"
        }
      ]
    }
  ]
}

Plaid's simpler fixed fixtures (user_good / pass_good) also work when you do not need specific accounts or transactions.

Mastercard US ("provider": "mastercard", "countryCode": "USA"): the hosted window is Finicity Connect. Search for FinBank Profiles - A and sign in with profile_02 / profile_02 for a profile that includes a credit card (mask 3333), or profile_03 / profile_03 for a depository-only profile. Select all accounts and save.

Mastercard EU ("provider": "mastercard", "countryCode": "DEU"): the hosted window is Mastercard Open Finance (the MTF test environment). On the "Choose your bank" screen, pick Test Banks, then Mock Bank. Sign in with User ID john.smith, choose Success on the authentication screen, and Allow and share data on the consent screen. Supported EU country codes include DEU, FRA, NLD, ESP, BEL, and PRT.

4. Create a bank link with the SDK

The Unified Enrollment SDK runs the same connect step embedded in your page. In sandbox, load the bundle from the sandbox host; it auto-detects the environment and calls the sandbox API:

<script src="https://sdk.sandbox.astrada.co/unified/v1/unifiedEnrollmentSdk.js"></script>
<script>
  UnifiedEnrollmentSdk.open({
    subaccountId: "{subaccountId}",
    companyName: "Your company",
    getAccessToken: () => fetch("/your-backend/astrada-sandbox-token").then((r) => r.text()),
    onEvent: (e) => console.log(e),
  });
</script>

Enable the bank rail on the subaccount's enrollmentPolicy (or open with a bank entry) as described in Choose your rails, then complete the provider window with the same test-bank fixtures as step 3. You get a bank-linked event with the connection and its accounts, plus one card-enrolled event per minted card with enrollmentType: "bank-feed".

🚧

Mint the token on your backend

getAccessToken must return a token minted server-side with your client credentials, exactly as in production. Minting from the browser fails with 403 {"error":"Invalid origin"} unless the calling origin is registered on your OAuth client.

5. See what was created

Completion is asynchronous but fast; poll the link until state is completed (the banklink.completed webhook carries the same account list):

curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "https://api.sandbox.astrada.co/bank-links/sandbox-walkthrough-1"

Response (200 OK): "state": "completed".

curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "https://api.sandbox.astrada.co/bank-accounts?subaccountId={subaccountId}"

Response (200 OK), trimmed: the discovered accounts. Credit-card accounts arrive with a minted bank-feed card already linked:

{
  "items": [
    {
      "id": "6c190c5b-d4bd-4c9c-ba3e-7eb6d4517e71",
      "mask": "4545",
      "type": "credit",
      "institutionName": "First Platypus Bank",
      "state": "active",
      "cardIds": ["a9381919-7073-4ba8-b96e-63440b94fd7b"]
    }
  ]
}

The minted card is a first-class card: GET /card-subscriptions?cardId={cardId} shows its subscription with enrollmentType: "bank-feed" and a bankAccountId back-reference, and a cardsubscription.created webhook fired for it. See Unified Card & Bank Feeds for the model.

6. Get bank transactions

A fresh link starts empty: GET /bank-transactions/{subaccountId} returns {"items": []} because the initial sync deliberately skips transactions dated before the link was created. Pull history with the backfill endpoint (scope banking:admin):

curl -X POST "https://api.sandbox.astrada.co/bank-accounts/{bankAccountId}/backfill" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Response (201 Created):

{
  "id": "6c190c5b-d4bd-4c9c-ba3e-7eb6d4517e71",
  "transactionsProcessed": 171,
  "matchesFound": 0
}

For Plaid accounts this pulls the fixture history (the default First Platypus accounts carry a rich synthetic history; override_accounts carries whatever you declared). For Mastercard US accounts, FinBank history must first be generated on the provider side: send {"loadHistoric": true} in the body (asynchronous, and billable against real providers in production; harmless in sandbox). New transactions also flow in continuously: sandbox Plaid links are refreshed about every 5 minutes.

curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "https://api.sandbox.astrada.co/bank-transactions/{subaccountId}?limit=10"

Response (200 OK): the synced transactions, each also announced by a banktransaction.created webhook.

7. Simulate a card transaction and reconcile

Bank linking's reconciliation matches card transactions against bank transactions. Simulate the card side against the minted card, using the simple_purchase scenario (merchant "Blue Bottle Coffee", default amount 4.50 USD, which is exactly the bank fixture from step 3):

curl -X POST https://api.sandbox.astrada.co/simulate/transaction/scenario \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "scenario": "simple_purchase",
    "subaccountId": "{subaccountId}",
    "cardId": "{cardId}"
  }'

Response (202 Accepted): the published messageIds. Give the pipeline a minute, then ask matching to run against the bank transaction from step 3 (find its id in the step 6 listing; scope banking:admin):

curl -X POST "https://api.sandbox.astrada.co/bank-transactions/{subaccountId}/webhooks" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": ["{bankTransactionId}"],
    "forceRematch": true,
    "webhooks": ["transaction.match.created"]
  }'

Response (200 OK):

{
  "message": "1 new match(es) created",
  "matched": 1,
  "emitted": 0,
  "resent": 0,
  "skipped": 0,
  "failed": []
}
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "https://api.sandbox.astrada.co/transaction-matches/{subaccountId}?minConfidence=LOW"

Response (200 OK), trimmed:

{
  "items": [
    {
      "id": "f573a0f6-a4da-4972-a605-d5b4687a3aa9",
      "confidence": "HIGH",
      "score": 1,
      "cardTransactionId": "...",
      "bankTransactionId": "..."
    }
  ]
}

A transaction.match.created webhook fired with the same match. Matching multiplies amount, date, and merchant similarity, and a date gap over 5 days scores zero, so give your bank fixture a recent date and a merchant that resembles the scenario's ("BLUE BOTTLE COFFEE" for simple_purchase); the scenario catalog with each merchant is in the Sandbox API Reference.

8. Webhooks checklist

By this point your receiver should have, in roughly this order:

EventFired at
banklink.completedStep 3/4 completion, with all discovered accounts
cardsubscription.createdSame completion, one per minted bank-feed card
bankaccount.state_changedSame completion (active)
banktransaction.createdStep 6, one per synced bank transaction
transaction.created / transaction.updatedStep 7, for the simulated card transaction
transaction.match.createdStep 7, when the match is created

Payload shapes are in Bank Linking Webhooks & Events.

9. Clean up

curl -X DELETE "https://api.sandbox.astrada.co/bank-links/sandbox-walkthrough-1" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response (204 No Content): the link, its bank accounts, and their transactions are deleted, and the provider connection is revoked. Deleting the subaccount itself (DELETE /subaccounts/{subaccountId}) now also clears its bank links, so a single call tears the whole test subaccount down — see Cleaning up test data.

Next steps


Did this page help you?