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
| Requires | What it delivers | |
|---|---|---|
| Bank-only events | Bank connected | Net-new SETTLED transactions for fees, transfers in/out, loan payments, income, and FX markup. Activity with no card-side counterpart. |
| Earlier settlement | Bank connected and card linked | Card authorizations flip PENDING→SETTLED on bank evidence, typically within hours of the swipe instead of days. |
| Card enrollment (bank rail) | Bank connected | A 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.createdand a SETTLEDtransaction.created), and you receive no card authorizations or match events. Completing the link still creates a card (announced bycardsubscription.createdwithenrollmentType: "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.
| Provider | provider | Bank countries (countryCode) |
|---|---|---|
| Plaid | plaid | USA, CAN |
| Mastercard Open Finance US | mastercard | USA |
| Mastercard Open Finance Europe | mastercard | European 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 namesThe
route-decidedevent andpersonanamed below are SDK v2. A v1 page shows the same screens and your backend gets the same webhooks; v1 pages have noroute-decidedand nopersona.
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.
- Dana types the first 8 digits of her Amex. The SDK sends them to
POST /enrollment-methodsandGET /bin-lookup, sees a 3-series card with the bank step allowed, and sendsroute-decidedwithroute: "amex-bank". The screen says "Connect your American Express card", with Open the Amex window. - The SDK creates the link.
POST /bank-linkswithfilters: { accountTypes: ["credit"], linkCustomizationName: "amex_only" }and the card's first six digits. The response carries ahostedLinkthat the SDK opens in a popup; it is valid for 4 hours and the SDK refreshes it when it is about to expire. - 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. - 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 itscardIds) and onecard-enrolledper card withenrollmentType: "bank-feed"; the screen says "Card connected". Your backend getsbanklink.completedand onecardsubscription.createdper card. - Spend arrives as Amex posts it. Each posting on the account arrives as
transaction.createdon the company card, with statusSETTLED: bank data comes after posting, not at the moment of purchase. - 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, samebankAccountIdas 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 has | What you get |
|---|---|
| One card on the account | One card at link, with all the spend. |
| Employee cards on the company account | The 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 each | Each is its own row in Amex's list, and its own card at link. |
| Linked through American Express - @ Work | Names 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
bankEnrollmentto"amex-only"(Amex only),"amex-visa"(Amex, plus consumer Visa cards) or"on"(every network), on the subaccount'senrollmentPolicyor inopen(); 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 sendscompany-connection-needed. - Scopes. The SDK token needs
banking:writeforPOST /bank-linksandPOST /bank-accounts/{id}/refresh, andbanking:readforGET /bank-links/{id}andGET /bank-accounts. - Country. The SDK sends
bankLink.countryCodewhen you set it, otherwise the card issuer's country. Plaid servesUSAandCAN. When no provider serves the country, Astrada refuses the link, the SDK routes the card again without the bank step and sends a secondroute-decided; noerrorfires. - Sandbox. Set
verificationPolicy.sandbox: trueon the test subaccount, so its links go to Plaid Sandbox. There, First Platypus Bank stands in for American Express; sign in withuser_good/pass_goodand 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}carrieslastRejection: { 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 sendserrorwithflow: "bank"andcode: "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
errorwithcode: "bank_link.timed_out". - The link could not be created.
errorwithcode: "bank_link.create_failed", and the screen offers to try again. - Amex asks the admin to sign in again. The bank account's
statebecomesauth_requiredandbankaccount.state_changedfires. Open the SDK withreconnect: { bankAccountId }, or callPOST /bank-accounts/{id}/refreshand send the admin to itshostedLink. The SDK reports the account reconnected only once it readsactiveagain, with abank-linkedevent for that account; an unknown id sendserrorwithcode: "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
noLinkReasononbank-linkedand onGET /bank-accountsuntil 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-linkswith the sameidthen makes a new one. onEventandonSuccessmirror the session in the browser. The durable record is your backend's:banklink.completedandcardsubscription.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
- Quick Start: Bank Linking: begin the integration.
- Testing Bank Linking: walk the whole flow in the sandbox environment.
- Webhook Event Types: the canonical event catalog.
Updated 3 days ago
