Quick Start: Bank Linking

Introduction

Bank linking unifies a cardholder's card and bank activity into one transaction stream. Once a cardholder connects their bank, the transactionmessage.created, transaction.created, and transaction.updated webhooks you already operate start carrying bank-side activity — fees, transfers, payroll, loan payments, foreign-exchange markup — in the same schema, on the same handlers. Card transactions on linked cards also settle earlier: hours after the swipe rather than days.

This page maps the whole journey end to end and links out to the detail. Each step is a real API call; the cardholder's only interaction is Plaid's hosted bank-connect flow.

📘

The two-step model

Bank linking is bank-first. The cardholder connects their bank, and that alone unlocks bank-only events (fees, transfers, payroll…). Linking cards is a second, optional step that adds earlier settlement for card spend — do it now, later, or never. The two are independent; see Linking Cards to Bank Accounts.

What you get

RequiresWhat it delivers
Bank-only eventsBank connectedNet-new SETTLED transactions for fees, transfers in/out, loan payments, income, 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.

Both ride the platform's existing webhooks — no new event types, no schema changes. Full payload detail is in Bank Linking Webhooks & Events.

The flow at a glance

sequenceDiagram
    autonumber
    participant You as Your app
    participant CH as Cardholder
    participant Astrada as Astrada API
    participant Plaid
    You->>Astrada: POST /bank-links (id + subaccountId)
    Astrada-->>You: hostedLink (Plaid, 4-hour TTL)
    You->>CH: Distribute the hostedLink
    CH->>Plaid: Choose bank, sign in, select accounts
    Plaid-->>Astrada: Session finished
    Astrada-->>You: banklink.completed (bankAccounts[])
    Note over You,Astrada: Bank-only events begin immediately
    You->>Astrada: (optional) PATCH /bank-accounts/{id} — link cards
    Note over You,Astrada: Linked cards now settle earlier

Steps

1. Authenticate

Get an access token for the Astrada API (all calls below go to the production gateway, https://api.astrada.co). Bank linking uses three scopes:

ScopeCovers
banking:readEverything readable — list links, list bank accounts, check state.
banking:writeThe per-cardholder write surface — create links, link/unlink cards, re-authenticate a connection.
banking:adminOperational and destructive actions — on-demand sync, disconnect.

See Authentication.

2. Create a bank link

POST /bank-links with your correlation id and the cardholder's subaccountId. The response carries a Plaid hostedLink you distribute to the cardholder; it has a 4-hour TTL.

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

{
  "id": "enroll-user-456",
  "subaccountId": "81e7c267-0927-4234-a2bd-78cef799de54"
}
curl -X POST https://api.astrada.co/bank-links \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "enroll-user-456",
    "subaccountId": "81e7c267-0927-4234-a2bd-78cef799de54"
  }'
{
  "id": "enroll-user-456",
  "subaccountId": "81e7c267-0927-4234-a2bd-78cef799de54"
}
FieldTypeRequiredDescription
idstring (alphanumeric + _/-, ≤256)YesYour correlation key. Map it back to your user/card row.
subaccountIdUUIDYesThe subaccount that routes events and scopes card linking.

Response (201 Created):

{
  "id": "enroll-user-456",
  "accountId": "d8fae7c0-0f5b-4d6b-8cce-71764e3f515f",
  "subaccountId": "81e7c267-0927-4234-a2bd-78cef799de54",
  "state": "pending",
  "hostedLink": "https://secure.plaid.com/link/lp9o97618r1r6p93oon62nr025950ro4s3",
  "linkExpiresAt": "2026-04-27T16:00:00Z",
  "createdAt": "2026-04-27T12:00:00Z"
}

3. The cardholder connects their bank

Distribute the hostedLink. The cardholder completes Plaid's hosted flow — there's no UI for you to build, and Astrada never sees their banking credentials. The four screens below are what the cardholder walks through.

a. Choose a bank

The cardholder searches for and selects their institution.

b. Sign in

Plaid collects the bank credentials and authenticates directly with the institution.

c. Select accounts

The cardholder chooses which accounts to share (checking, savings, …).

d. Done

Plaid confirms the connection and returns the cardholder to your flow.

🚧

Credentials never touch Astrada

The cardholder authenticates inside Plaid's UI. Plaid hands Astrada an opaque access token — the bank username and password never enter Astrada's systems. See Privacy below.

4. Detect completion

There are two robust ways to know the cardholder finished, and you can use either:

Webhook — banklink.completed:

{
  "bankLinkId": "enroll-user-456",
  "bankAccounts": [
    { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Chase Checking" },
    { "id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "name": "Chase Savings" }
  ]
}

Polling — GET /bank-links/{id} (scope banking:read): this endpoint doubles as a completion detector. If the cardholder finished but the webhook hasn't arrived, calling GET transitions the link to completed and returns the discovered accounts inline. It also transparently mints a fresh hostedLink if you GET a pending link whose linkExpiresAt has passed — you never handle expiration by hand.

{
  "id": "enroll-user-456",
  "state": "completed",
  "hostedLink": null,
  "completedAt": "2026-04-27T14:30:00Z",
  "bankAccounts": [
    { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Chase Checking" },
    { "id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321", "name": "Chase Savings" }
  ]
}

At this point bank-only events already fire on the connected accounts. Read the full account records — including the cardIds currently linked to each — with GET /bank-accounts?subaccountId={subaccountId}.

5. Link cards for earlier settlement (optional)

Linking a card to a bank account is what turns on earlier settlement for that card. In most integrations you don't have to do anything: card links are detected automatically the first time a card's transactions produce a high-confidence match against a bank account's feed.

To pre-link a card before any spend exists — or to correct an assignment — call PATCH /bank-accounts/{id} with the desired cardIds:

PATCH /bank-accounts/a1b2c3d4-e5f6-7890-abcd-ef1234567890 HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
  "cardIds": ["5dec2c49-0aa5-4683-a317-427eb5d115f3"]
}
curl -X PATCH https://api.astrada.co/bank-accounts/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "cardIds": ["5dec2c49-0aa5-4683-a317-427eb5d115f3"]
  }'
{
  "cardIds": ["5dec2c49-0aa5-4683-a317-427eb5d115f3"]
}

Multi-card linking, backfill, and the linking constraints (a card links to one account at a time, etc.) are covered in Linking Cards to Bank Accounts.

📘

Bank-only mode

You can skip card linking entirely and use bank linking as a standalone bank-data source. Every bank transaction still surfaces (as banktransaction.created and a SETTLED transaction.created), but with no card you won't receive card authorizations or transaction.match.created. See Bank-only mode.

6. Receive events

From here, bank activity flows on the webhooks you already handle. A payroll deposit arrives as a transaction.created; a linked card's swipe settles early as a transaction.updated. New to Astrada webhooks? Register an endpoint with POST /webhooks, listing the event types you want to receive. See Bank Linking Webhooks & Events for every event and its payload.

Keep connections healthy

Plaid connections occasionally need re-authentication — a changed bank password, an expiring consent, or an MFA refresh. When that happens the bank account's state flips to auth_required, a bankaccount.state_changed webhook fires with the reason, and bank-driven events stop for that account until the cardholder re-connects.

  • Detect it: subscribe to bankaccount.state_changed, or poll GET /bank-accounts?state=auth_required (hourly is plenty).
  • Fix it: POST /bank-accounts/{id}/refresh (scope banking:write — re-auth is customer self-serve) returns a fresh Plaid update-mode hosted link. The cardholder completes the same Plaid flow to re-authenticate; the bank account ID stays the same and events resume on the next sync.
  • When it can't be fixed: if the connection is revoked or the item is permanently lost, the account goes to disconnected instead — refresh won't recover it, so you disconnect it and re-enroll with a new bank link.

Disconnect a connection

When a cardholder opts out — or you need to clear a disconnected connection before re-enrolling — tear it down. Both calls take scope banking:admin.

  • Remove the whole connection: DELETE /bank-links/{id} revokes the item with Plaid and deletes its bank accounts and their transactions. Returns 204 No Content.
  • Remove a single account: DELETE /bank-accounts/{id} drops one bank account and its transactions while leaving the rest of the connection intact. Returns 204 No Content.
📘

On-demand sync

Bank data syncs on its own — once when the link completes, then on Plaid's update webhooks — so you rarely pull by hand. When you need data now (e.g. reconciling right after a known deposit), POST /bank-links/{id}/sync (scope banking:admin) forces a sync immediately.

Privacy

Three guarantees, each with a concrete control:

  • Astrada never stores Plaid credentials. The cardholder authenticates inside Plaid's hosted UI; Astrada holds only the opaque access token Plaid returns.
  • Account numbers are stored only as a short mask. Every bank account exposes mask (typically the last 4 digits). The full account number and routing number are not stored, logged, or returned in any payload.
  • Subaccount scoping is enforced at the routing layer. Every event fans out only to subscribers scoped to the originating subaccountId — no cross-subaccount leakage.

Next steps