Unified Card & Bank Feeds

Introduction

Astrada represents every enrolled card the same way, whether it is a Visa, Mastercard, or American Express card: a card with a durable cardId, a card subscription that announces the enrollment, and transactions that carry that cardId. What differs per network is the transport. Visa and Mastercard data travels over direct network connections, while American Express data typically travels through a bank connection that the cardholder authorizes. This page explains which rail serves which network, what data each rail delivers, and why the order in which cards and bank connections arrive does not change what you build.

📘

Assign cards to cardholders before the first transaction, on every rail

Completing a bank link does more than connect an account. For each credit account it discovers
that carries a usable last 4 and does not resolve cleanly to a single already-enrolled card,
Astrada creates a real card, with a real cardId and an active card subscription, at
completion time. You can assign that cardId to the
right cardholder in your system immediately, before any transaction exists.

The model

Three resources are involved (field-level detail: Core Resources):

  • Card is the durable identity. Its cardId is what every transaction references, on every rail, for the card's lifetime.
  • Card subscription is created once per enrollment and announced by the cardsubscription.created webhook. Its enrollmentType records how the enrollment happened: cardholder-single (the cardholder entered the number), network-bulk (captured from the network's data sources), or bank-feed (created from a bank connection). Everything else about the contract is the same.
  • Bank account is a connected account at a financial institution, created when a bank link completes. For credit accounts, its mask is the card number's last 4.

The two card lists on a bank account

A bank account response lists cards in two places, and they answer different questions.

cardIds answers: which cards are linked to this account? A card gets linked when its last 4 digits match the account's mask at bank-link completion, when its transactions produce a high-confidence match against the account's feed, or when you link it manually via PATCH. Linked cards can come from any rail.

cards[] answers: which cards were created from this account? When a bank link completes and a credit account with a usable last 4 does not resolve cleanly to a single enrolled card, Astrada creates a card from that account. The new card appears here, together with its subscription.

A created card is also linked to its own account, so it shows up in both lists. A card that was matched or manually linked shows up in cardIds only. A credit account behind an American Express connection reads like this:

{
  "id": "059d5e6a-cfb1-46e6-ab84-6586582b6b58",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "bankLinkId": "enrollment_amex_7f2c",
  "institutionName": "American Express",
  "institutionId": "ins_10",
  "name": "Platinum Card",
  "type": "credit",
  "mask": "1005",
  "currency": "USD",
  "state": "active",
  "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"
    }
  ]
}

The card's last4digits equals the account's mask by construction, and network is set when the institution implies it (as it does here).

Choosing the rail per network

RailNetworksHow enrollment startsenrollmentTypeTransaction data you receive
Single card enrollmentVisa, MastercardThe cardholder enters their card number; 3DS verification activates the subscription. Quick Start: Card Enrollmentcardholder-singleReal-time authorizations as the cardholder spends, followed by the settlement. Fields include amount and currency, merchant name, category (MCC), and location.
Network bulk feedVisa, Mastercard, American Express (eligible corporate programs)A whole card program is captured from the network's data sources (e.g. Mastercard Smart Data). Many cards at once, no cardholder interaction.network-bulkThe same transaction contract, delivered from the network's feed. Cardholder records (name, employee ID) arrive populated when the program's feed carries them.
Bank-connection enrollmentAmerican ExpressThe cardholder connects the account behind their card through the hosted bank-linking flow; completion creates a card per credit account that exposes a usable last 4. Quick Start: Bank Linkingbank-feedSettled transactions from the bank connection. Fields include amount, merchant name, dates, and category. No real-time authorization stream.

Pick the entry point per network; everything downstream is shared. For Visa and Mastercard, enroll by card number (POST /card-subscriptions, see Quick Start: Card Enrollment) or by bulk feed at the program level. For American Express, create a bank link (POST /bank-links) and let the cardholder connect the account behind the card. A unified enrollment SDK that detects the network and routes this automatically is coming soon.

The three orderings

Cards and bank connections can arrive in any order. Each ordering ends in the same state: one cardId per card, one subscription per enrollment, one transaction stream. None of them needs special handling. Here is what happens in each.

Bank first: connect, then assign

Create the bank link. For an American Express enrollment, narrow the hosted session to credit accounts, and pass the first 6 digits if your enrollment UI captured them before pivoting from card entry into the bank flow (they are attached to the created card when the connection creates exactly one):

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

{
  "id": "enrollment_amex_7f2c",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "filters": { "accountTypes": ["credit"] },
  "bin6": "371449"
}
curl -X POST https://api.astrada.co/bank-links \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "enrollment_amex_7f2c",
    "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
    "filters": { "accountTypes": ["credit"] },
    "bin6": "371449"
  }'
{
  "id": "enrollment_amex_7f2c",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "filters": { "accountTypes": ["credit"] },
  "bin6": "371449"
}

The cardholder completes the hosted flow (see Quick Start: Bank Linking). At completion, each discovered credit account that carries a usable last 4 and does not resolve cleanly to a single already-enrolled card creates a card: a real cardId plus an active bank-feed subscription, before any transaction exists. Two webhooks tell you about it; treat them as independent, since delivery order is not guaranteed. banklink.completed fires exactly once for the completion, announcing the connection with each account's mask, type, and cards, including the created ones. One delivery carries every account, so there are no duplicate completion events to coalesce:

{
  "bankLinkId": "enrollment_amex_7f2c",
  "bankAccounts": [
    {
      "id": "059d5e6a-cfb1-46e6-ab84-6586582b6b58",
      "name": "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"
        }
      ]
    }
  ]
}

And cardsubscription.created fires for each created card. It is the same event every other rail emits, with enrollmentType: "bank-feed" and a bankAccountId reference back to the source account:

{
  "_links": {
    "card": { "href": "/cards/9e107d9d-372b-4d1a-9f4a-654321abcdef" },
    "bankAccount": { "href": "/bank-accounts/059d5e6a-cfb1-46e6-ab84-6586582b6b58" },
    "self": { "href": "/card-subscriptions/1b671a64-40d5-491e-99b0-da01ff1f3341" }
  },
  "cardId": "9e107d9d-372b-4d1a-9f4a-654321abcdef",
  "enrollmentType": "bank-feed",
  "bankAccountId": "059d5e6a-cfb1-46e6-ab84-6586582b6b58",
  "networkBulkFeedId": null,
  "id": "1b671a64-40d5-491e-99b0-da01ff1f3341",
  "state": "active",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "createdAt": "2026-07-14T12:00:00.000Z",
  "effectiveDate": "2026-07-14T12:00:00.000Z",
  "expirationDate": "2099-12-31T23:59:59.999Z",
  "customerReferenceId": null,
  "updatedAt": "2026-07-14T12:00:00.000Z"
}

Assign the cardId to your cardholder now. The card and its subscription exist before the first swipe, so every transaction that arrives later is already attributed to the right person.

Card first, bank connected later

If a card is already enrolled when its bank connection completes, the account resolves to the existing card instead of creating a duplicate identity: at completion, each discovered credit account's mask is compared against the subaccount's enrolled cards, and the card is linked when its last 4 match exactly one way. A mask matching multiple cards, or multiple accounts claiming the same mask, links nothing. Rather than risk a wrong merge, such an account gets its own bank-feed card so its feed still has an identity, and automatic transaction-based discovery remains the backstop for the enrolled cards. The completed bank link shows the result on its account summaries:

{
  "id": "enrollment_9d21",
  "state": "completed",
  "completedAt": "2026-07-14T12:00:00.000Z",
  "bankAccounts": [
    {
      "id": "3a8f2c1d-5e7b-4d9a-b6c8-9f0e1d2a3b4c",
      "name": "Corporate Card",
      "type": "credit",
      "mask": "4001",
      "cardIds": ["5dec2c49-0aa5-4683-a317-427eb5d115f3"]
    }
  ]
}

The enrolled card's data keeps flowing exactly as before: same cardId, same subscription, same stream. What the link adds is bank-side evidence for that card's spend, which enables matching and earlier settlement, per Linking Cards to Bank Accounts.

Bank first, card enrolled later

The reverse order works too. If a card that was created from a bank connection is later enrolled by its number, or arrives in a network bulk feed, the enrollment resolves to the same cardId: the card is upgraded in place, not duplicated. The merge is deliberately conservative: it happens when the enrollment matches exactly one bank-derived card (same last 4, compatible network), and anything ambiguous keeps separate cards, because two identities are correctable and a wrong merge is not. The new enrollment announces itself the standard way, with a cardsubscription.created for the same card:

{
  "_links": {
    "card": { "href": "/cards/9e107d9d-372b-4d1a-9f4a-654321abcdef" },
    "self": { "href": "/card-subscriptions/aadf6c89-0356-4d94-8fa8-b5465077a0fb" }
  },
  "cardId": "9e107d9d-372b-4d1a-9f4a-654321abcdef",
  "enrollmentType": "cardholder-single",
  "bankAccountId": null,
  "networkBulkFeedId": null,
  "id": "aadf6c89-0356-4d94-8fa8-b5465077a0fb",
  "state": "reqSCA",
  "subaccountId": "6103874b-248d-4825-af88-a0d24dd9b123",
  "createdAt": "2026-07-14T12:00:00.000Z",
  "effectiveDate": "2026-07-14T12:00:00.000Z",
  "expirationDate": "2027-07-14T12:00:00.000Z",
  "customerReferenceId": null,
  "updatedAt": "2026-07-14T12:00:00.000Z"
}

What changes: the card fills in the facts only the new rail can supply (network where it was null, the first 6 digits), and once the new subscription activates, that rail's data begins. For a card-number enrollment that means the real-time authorization stream. What does not change: the cardId, the bank feed (which keeps flowing), and every reference you have stored. Richer data starts arriving under the identity you already assigned. No re-integration, no deduplication on your side.

Transactions

Every transaction on an enrolled card, on every rail, carries the cardId (and a _links.card reference); bank-sourced activity on an account with no card is the one exception, covered in Event Types. One attribution model covers the whole stream. Per rail:

  • Single card enrollment: network messages (authorization, advice, reversal) arrive keyed to the enrolled card and aggregate into its transactions. transactionmessage.created and transaction.created / transaction.updated all carry the cardId.
  • Network bulk feed: the network's feed delivers each transaction already attributed to its bulk-enrolled card. Handling is identical to single enrollment; only the subscription's enrollmentType differs.
  • Bank-connection enrollment: postings sync from the connection and surface as bank-sourced transaction messages attributed to the account's card, aggregating into SETTLED transactions with that cardId. Payload markers for bank-sourced activity (the bankData block, bank_-prefixed references) are covered in Event Types.
🚧

When one account carries several physical cards

Granularity on the bank rail is the institution's. Most card institutions expose one credit
account per card, each with its own last 4, so each physical card gets its own created card. When
an institution combines multiple physical cards into a single account, the connection carries one
card identity per account: transactions attribute to that account's card, not to each piece of
plastic. The account's mask is not guaranteed to be per-card in that case. An account that
exposes no usable last 4 gets no created card at all: its postings remain available as bank
transactions but do not join the card-attributed stream.

Integrating

The whole model above is consumable with webhooks plus REST. Register for the events with POST /webhooks (see Webhooks), then:

  • Watch cardsubscription.created. It is the enrollment signal on every rail and fires once per enrollment: a card-number enrollment produces one, a bulk feed produces one per card in the program, and a completed bank link produces one per created card. There is no separate event type for bank-derived cards; enrollmentType: "bank-feed" on this event is the enrollment on that transport.
  • Expect one banklink.completed per completion. A finished bank link delivers a single completion event that lists every account it discovered and every card it created. Key it by bankLinkId; there are no duplicates to merge.
  • Store cardId as your durable key. Assign it to your cardholder as soon as the event arrives. Subscriptions come and go (re-enrollment creates a new one for the same card), but the card's identity survives every rail transition.
  • Treat enrollmentType as display metadata. It is useful for labeling and analytics, never for control flow. The payloads differ only in their back-references (networkBulkFeedId for bulk, bankAccountId for bank-feed) and initial state: a bank-feed subscription is born active, while a card-number enrollment starts at reqSCA until 3DS completes.

Because every rail shares these events, one handler covers all of them. Switch on the event type and read enrollmentType as a label, never a branch:

// One endpoint, every rail. Register it once with POST /webhooks.
// eventType comes from the `webhook-event-type` header on each delivery.
export function handleAstradaWebhook(eventType, body) {
  switch (eventType) {
    case "cardsubscription.created": {
      // Fires once per enrollment on every rail. The card already exists,
      // so assign it to your cardholder now, before the first transaction.
      // enrollmentType is "cardholder-single" | "network-bulk" | "bank-feed";
      // read it for labels/analytics, don't branch your logic on it.
      assignCardToCardholder(body.cardId);
      break;
    }
    case "banklink.completed": {
      // Exactly one per completed link, carrying every account and the
      // cards created from them. No duplicate completions to coalesce.
      for (const account of body.bankAccounts) {
        for (const card of account.cards ?? []) {
          assignCardToCardholder(card.id); // same mapping as above
        }
      }
      break;
    }
    // transaction.created / transaction.updated carry the same cardId. Attribute by it.
  }
}

For the step-by-step walkthrough of these calls and events in a working integration, see Getting Started.

FAQ

Can I assign a card to a cardholder before any spend exists?

Yes, on every rail. Card-number enrollment and bulk feeds hand you the cardId at enrollment, and completing a bank link creates a real cardId at completion time for each credit account with a usable last 4. In all three cases the identity exists before the first transaction, so your card-to-cardholder mapping is in place when spend starts.

What happens if American Express single enrollment becomes available later?

Nothing you need to prepare for. A bank-derived card later enrolled by its number (or delivered in a bulk feed) resolves to the same cardId and is upgraded in place whenever the match is unambiguous: exactly one bank-derived card with those last 4 digits. Richer data starts flowing under the identity you already stored, the bank feed keeps running, and no records need re-mapping or deduplication.

Do I need different webhook handling per rail?

No. Every rail emits cardsubscription.created for enrollment and attributes every transaction to a cardId on the same transactionmessage.created / transaction.created / transaction.updated events. One handler set covers all three rails. enrollmentType is there to read, not to branch on.


Did this page help you?