Bank Linking

Introduction

Bank linking connects a cardholder's bank account through an open-banking provider (Plaid or Mastercard Open Finance) so their bank activity flows into the same transaction stream as their card spend. It gives you three things: net-new bank-only events (fees, transfers, payroll, loan payments, foreign-exchange markup), earlier settlement of card transactions, and card enrollment over the bank rail. The transaction stream rides the transactionmessage.created, transaction.created, and transaction.updated webhooks you already operate, with no new event types required, and created cards announce themselves on cardsubscription.created. Bank linking also emits a few dedicated events (banklink.completed, bankaccount.state_changed, and the bank-record events) covered in Bank Linking Webhooks & Events.

What you get

RequiresWhat it delivers
Bank-only eventsBank connectedNet-new SETTLED transactions for fees, transfers in/out, loan payments, income, and FX markup. Activity with no card-side counterpart.
Earlier settlementBank connected and card linkedCard authorizations flip PENDING→SETTLED on bank evidence, typically within hours of the swipe instead of days.
Card enrollment (bank rail)Bank connectedA real card and an active bank-feed subscription created for each credit account with a usable last 4 that does not resolve cleanly to a single already-enrolled card; cardsubscription.created announces each.

Bank linking is bank-first: connecting the bank enables bank-only events on its own, and linking cards is an optional second step that adds earlier settlement for card spend.

Two ways to use it

  • Alongside a card program: link a bank and cards to get bank-only events plus earlier settlement of card transactions.
  • Bank-only: link a bank without enrolling any card numbers and use it as a standalone bank-data source in place of card enrollment. Every bank transaction still surfaces (via banktransaction.created and a SETTLED transaction.created), and you receive no card authorizations or match events. Completing the link still creates a card (announced by cardsubscription.created with enrollmentType: "bank-feed") for each credit account with a usable last 4, and that account's activity is attributed to that card. See Bank-only mode.

Providers

Each bank link runs through one open-banking provider, which handles the cardholder's bank sign-in and supplies the bank data. What Astrada returns is the same for every provider: the same bank link and bank account resources, the same account states, and the same webhooks.

ProviderproviderBank countries (countryCode)
PlaidplaidUSA, CAN
Mastercard Open Finance USmastercardUSA
Mastercard Open Finance EuropemastercardEuropean countries, such as DEU, FRA, NLD, ESP, BEL, PRT

You pick the provider for a link with provider and countryCode on POST /bank-links, or with the bankLink option of the Unified Enrollment SDK. A link that names no provider uses your subaccount's default when Astrada has set one, and otherwise Plaid in the USA. Which providers you can use is part of your account setup, so talk to your Astrada contact before using Mastercard Open Finance in production. For Mastercard Open Finance US, Astrada first sets your account up with Mastercard.

American Express cards

An American Express card does not enroll with the card form: its data comes through the cardholder's Amex login. In the Unified Enrollment SDK, a card number starting with 3 goes to the bank step, the person signs in at American Express in a Plaid window, and Astrada turns each credit account they tick into a card with a bank-feed subscription. Purchases arrive once American Express posts them.

📘

SDK v2 names

The route-decided event and persona named below are SDK v2. A v1 page shows the same screens and your backend gets the same webhooks; v1 pages have no route-decided and no persona.

Short answers

Who signs in?On an admin page, one admin, once: their link covers every card on the accounts they tick. On an employee page (persona: "cardholder" in SDK v2), each employee signs in to their own Amex login.
Which login?American Express, not American Express - @ Work. Through @ Work, Amex sends the spender's name but not the card's last 4, so every transaction stays on the company card.
What do I get, and when?At link: one card per Amex account the person ticked (its last 4 is the account's), announced by cardsubscription.created. After that: a transaction.created on that card for each posting. With employee cards on for your account, one more card per employee who spends, within 30 minutes of their first synced transaction.
What allows it?bankEnrollment set to "amex-only", "amex-visa" or "on", on the subaccount's enrollmentPolicy or in open(). A debit or prepaid Amex gets no bank step.

One company, in order

Vertex Industries has one American Express account: the company card ending 1005, and employee cards ending 1018, 1026 and 1034 on the same account. Their admin, Dana, opens your admin page.

  1. Dana types the first 8 digits of her Amex. The SDK sends them to POST /enrollment-methods and GET /bin-lookup, sees a 3-series card with the bank step allowed, and sends route-decided with route: "amex-bank". The screen says "Connect your American Express card", with Open the Amex window.
  2. The SDK creates the link. POST /bank-links with filters: { accountTypes: ["credit"], linkCustomizationName: "amex_only" } and the card's first six digits. The response carries a hostedLink that the SDK opens in a popup; it is valid for 4 hours and the SDK refreshes it when it is about to expire.
  3. Dana picks American Express, signs in and ticks the account. Amex lists one row per account, not per employee card. The SDK shows "Finish up in the Amex window" and reads GET /bank-links/{id} every 3 seconds.
  4. Astrada completes the link. Each ticked credit account with a 4-digit last 4 becomes a card. Your page gets bank-linked (the accounts, each with its cardIds) and one card-enrolled per card with enrollmentType: "bank-feed"; the screen says "Card connected". Your backend gets banklink.completed and one cardsubscription.created per card.
  5. Spend arrives as Amex posts it. Each posting on the account arrives as transaction.created on the company card, with status SETTLED: bank data comes after posting, not at the moment of purchase.
  6. Employee cards, when Astrada has turned them on for your account. Every transaction names the spender and their card's last 4. Twice an hour Astrada makes a card for each new last 4 (cardsubscription.created, same bankAccountId as the company card), and that person's spend lands on it from then on. A transaction that arrives before its employee's card exists waits for the card, up to 24 hours, then lands on the company card instead.
sequenceDiagram
    autonumber
    participant P as Your page (SDK)
    participant A as Astrada API
    participant X as Plaid (Amex login, then the account data)
    participant B as Your backend
    P->>A: POST /enrollment-methods + GET /bin-lookup
    Note over P: route-decided: amex-bank
    P->>A: POST /bank-links (credit, amex_only, bin6)
    A-->>P: hostedLink, valid 4 hours
    P->>X: opens the hosted link in a popup
    X->>X: Dana signs in at Amex, ticks the account
    X-->>A: session finished
    loop every 3 s
        P->>A: GET /bank-links/{id}
    end
    Note over A: one bank-feed card per credit account with a last 4
    A-->>P: state completed, bankAccounts with cardIds
    Note over P: bank-linked, then card-enrolled per card
    A-->>B: banklink.completed, cardsubscription.created per card
    X-->>A: account postings, as Amex posts them
    A-->>B: transaction.created on the card

Which kind of account is it

What the company hasWhat you get
One card on the accountOne card at link, with all the spend.
Employee cards on the company accountThe company card at link; with employee cards on, one card per employee within 30 minutes of their first synced transaction, all sharing the company card's bankAccountId.
Employee cards with their own Amex account eachEach is its own row in Amex's list, and its own card at link.
Linked through American Express - @ WorkNames but no last 4, so nothing to split by: all spend stays on the company card.

How you tell: banklink.completed lists the accounts and their cardIds; a later cardsubscription.created with enrollmentType: "bank-feed" and a bankAccountId you already hold is an employee card on that account; GET /cards/{cardId}/cardholder gives the name as Amex prints it, so match employees by name.

The same story in API payloads

Trimmed to the fields that matter; the ids are made up.

The SDK creates the link (your backend can make the same call from a page of its own):

{
  "id": "sdk_6f1c2a3b-4d5e-4f60-8a7b-9c0d1e2f3a4b",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "filters": { "accountTypes": ["credit"], "linkCustomizationName": "amex_only" },
  "bin6": "371449",
  "countryCode": "USA"
}

At link, your page gets the accounts and the card Astrada made for each:

{
  "type": "bank-linked",
  "bankLinkId": "sdk_6f1c2a3b-4d5e-4f60-8a7b-9c0d1e2f3a4b",
  "bankAccounts": [
    {
      "id": "059d5e6a-cfb1-46e6-ab84-6586582b6b58",
      "name": "Business Platinum Card",
      "mask": "1005",
      "institutionName": "American Express",
      "type": "credit",
      "cardIds": ["9e107d9d-372b-4d1a-9f4a-654321abcdef"]
    }
  ]
}
{
  "type": "card-enrolled",
  "cardId": "9e107d9d-372b-4d1a-9f4a-654321abcdef",
  "subscriptionId": "1b671a64-40d5-491e-99b0-da01ff1f3341",
  "enrollmentType": "bank-feed",
  "scheme": "AMEX",
  "last4": "1005",
  "bankAccountId": "059d5e6a-cfb1-46e6-ab84-6586582b6b58"
}

Your backend hears the same from the webhooks. The casing differs between the two surfaces: the SDK event says scheme: "AMEX", the webhook says network: "amex", each as its own schema defines it. banklink.completed fires once, with every account and the cards made at completion:

{
  "bankLinkId": "sdk_6f1c2a3b-4d5e-4f60-8a7b-9c0d1e2f3a4b",
  "bankAccounts": [
    {
      "id": "059d5e6a-cfb1-46e6-ab84-6586582b6b58",
      "name": "Business Platinum Card",
      "mask": "1005",
      "type": "credit",
      "cardIds": ["9e107d9d-372b-4d1a-9f4a-654321abcdef"],
      "cards": [
        {
          "id": "9e107d9d-372b-4d1a-9f4a-654321abcdef",
          "subscriptionId": "1b671a64-40d5-491e-99b0-da01ff1f3341",
          "last4digits": "1005",
          "network": "amex",
          "enrollmentType": "bank-feed"
        }
      ]
    }
  ]
}

An employee card arrives later as its own subscription, on the same bank account:

{
  "id": "2c7e9d14-5c3a-4f8e-9b21-0d6c4a8e7f35",
  "cardId": "7d1f3a9b-0e2c-4b5d-8f61-2a3b4c5d6e7f",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "state": "active",
  "enrollmentType": "bank-feed",
  "bankAccountId": "059d5e6a-cfb1-46e6-ab84-6586582b6b58"
}

What you configure

  • The bank step. Set bankEnrollment to "amex-only" (Amex only), "amex-visa" (Amex, plus consumer Visa cards) or "on" (every network), on the subaccount's enrollmentPolicy or in open(); the subaccount's value wins. With "off", an Amex card on an admin page goes to the bulk kickoff when bulk is on, or to "This card can't be connected here"; on an employee page it shows "Your company connects this card" and sends company-connection-needed.
  • Scopes. The SDK token needs banking:write for POST /bank-links and POST /bank-accounts/{id}/refresh, and banking:read for GET /bank-links/{id} and GET /bank-accounts.
  • Country. The SDK sends bankLink.countryCode when you set it, otherwise the card issuer's country. Plaid serves USA and CAN. When no provider serves the country, Astrada refuses the link, the SDK routes the card again without the bank step and sends a second route-decided; no error fires.
  • Sandbox. Set verificationPolicy.sandbox: true on the test subaccount, so its links go to Plaid Sandbox. There, First Platypus Bank stands in for American Express; sign in with user_good / pass_good and tick the credit card account. See Testing Bank Linking. Sandbox links accept Plaid's stand-in institutions in place of American Express, so the wrong-bank refusal below does not fire for First Platypus Bank.

When it does not go to plan

  • Wrong bank picked. The link was made for American Express, so a session completed at another bank is refused before anything is created: the link stays pending, GET /bank-links/{id} carries lastRejection: { institutionName, at }, and the same hosted link can be used again. The SDK shows "That doesn't look like an American Express account" with Start over, and sends error with flow: "bank" and code: "bank_link.wrong_institution".
  • The window closes early. The SDK shows "Confirming your Amex connection" and keeps reading the link: every 2 seconds for 2 minutes, then every 10 seconds; after 5 minutes it says "We haven't confirmed your connection yet", with Keep waiting and Start over. A connection finished in the window still completes while the SDK waits.
  • Nothing happens for 10 minutes. The screen says "Still there?" with Open a new Amex window, and the SDK sends error with code: "bank_link.timed_out".
  • The link could not be created. error with code: "bank_link.create_failed", and the screen offers to try again.
  • Amex asks the admin to sign in again. The bank account's state becomes auth_required and bankaccount.state_changed fires. Open the SDK with reconnect: { bankAccountId }, or call POST /bank-accounts/{id}/refresh and send the admin to its hostedLink. The SDK reports the account reconnected only once it reads active again, with a bank-linked event for that account; an unknown id sends error with code: "bank_account.not_found".

Limits and gotchas

  • Astrada creates a card only for a credit account with a 4-digit last 4 that no enrolled card already holds. A credit account left without a card carries a noLinkReason on bank-linked and on GET /bank-accounts until one is made; depository accounts never get a card.
  • A 3-series card is routed as American Express; Diners Club and JCB are not told apart.
  • An Amex debit or prepaid card gets no bank step.
  • Employee cards are per account: ask your Astrada contact to turn them on. Until then every posting on the account lands on the company card.
  • Through @ Work, nothing carries a last 4, so employee cards cannot be made: point admins at American Express in your setup email. A company already linked through @ Work can link again through American Express.
  • The hosted link is valid for 4 hours; a pending link is expired 72 hours after that, and POST /bank-links with the same id then makes a new one.
  • onEvent and onSuccess mirror the session in the browser. The durable record is your backend's: banklink.completed and cardsubscription.created.

In this section

  • Quick Start: Bank Linking is the full on-ramp: create a link, the cardholder signs in to their bank, discover accounts, link cards, and receive events. Start here.
  • Linking Cards to Bank Accounts covers automatic detection and manual card linking, historical backfill, and the multi-card rules and constraints.
  • Bank Linking Webhooks & Events details every event the flow emits, the bank-only vs earlier-settlement behaviors, and where each payload lives.
  • Error States & Remediation lists the bank connection states (active, auth_required, disconnected), what to do in each, and how to reconnect.
  • Unified Card & Bank Feeds explains the card model shared across card-number enrollment, bulk feeds, and bank connections: cards created at completion, the two card lists on a bank account, and enrollment convergence.
  • Embed it instead: the Unified Enrollment SDK covers the same connect step inside one embedded flow, alongside card enrollment.

Next steps


Did this page help you?