Migrating to the Unified SDK

What's involved

The Unified Enrollment SDK replaces the legacy Card Enrollment SDK with one flow that covers card entry, bank connections, and network bulk feeds. Migrating is client-side work only: swap the script, rename the global, and rework the callbacks. Your backend keeps working as is, with the same token endpoint, the same webhooks, and the same cardId and subscriptionId.

Out of the box the unified SDK behaves like the legacy one: the card form and 3DS verification, full-screen or in your container. Bank linking and bulk feeds stay off until you enable them server-side, so you can migrate first and adopt the new rails later with no further client release. See Choose your rails.

📘

The unified SDK is in beta

What stays the same

  • Authentication. The getAccessToken contract is unchanged: an async function returning a token minted by your backend, with the same 5000 ms default timeout (getAccessTokenTimeoutMs to raise it). Never ship a client secret to the browser.
  • Config options. companyName, subaccountId, terms, statusLabels (same four stages), customerReferenceId, container, and metadata carry over unchanged.
  • onConnect. Fires at the same moment in both SDKs: when the cardholder submits the card form and verification begins. An existing handler carries over as is.
  • Theming. Every legacy setStyle variable keeps its name and effect. The unified SDK adds more surfaces (headlines, progress bar, chooser tiles, status chips); see Make it yours.
  • Everything server-side. Webhooks (cardsubscription.created), REST resources, ids, and the subaccount's verificationPolicy are untouched. A backend that assigns cards from webhooks needs no changes.

What changes

LegacyUnified
Scripthttps://sdk.astrada.co/v1/cardEnrollmentSdk.jshttps://sdk.astrada.co/unified/v1/unifiedEnrollmentSdk.js
Script data-idcard-enrollment-sdkunified-enrollment-sdk
GlobalCardEnrollmentSdkUnifiedEnrollmentSdk
Open / closeopenForm(config) / closeForm()open(config) / close()
TypeScript types@astrada.co/card-enrollment-sdk@astrada.co/card-enrollment-sdk/unified (same package, new entry point)
onSuccessOne card, at the moment it enrollsThe whole session, once, at close
onError payload{ type, error: { detail, errorCode } }{ flow, code, message }, and the session stays open
onCancelCardholder closed the formSession closed with nothing enrolled
New optionsonEvent, entry, rail preferences, linkAnotherCard, maxWidth

The renames are mechanical. The three callback rows change behavior, so take them one at a time below.

The swap

<script
  type="text/javascript"
  src="https://sdk.astrada.co/v1/cardEnrollmentSdk.js"
  data-id="card-enrollment-sdk"
></script>
<script>
  CardEnrollmentSdk.openForm({
    companyName: "Your company",
    subaccountId: "<subaccountId>",
    getAccessToken: async () => "<access token>",
    onSuccess: (data) => assignCard(data.cardId, data.subscriptionId),
    onError: (data) => showError(data.error.detail),
    onCancel: () => {},
  });
</script>
<script
  type="text/javascript"
  src="https://sdk.astrada.co/unified/v1/unifiedEnrollmentSdk.js"
  data-id="unified-enrollment-sdk"
></script>
<script>
  UnifiedEnrollmentSdk.open({
    companyName: "Your company",
    subaccountId: "<subaccountId>",
    getAccessToken: async () => "<access token>",
    onSuccess: (result) => {
      for (const card of Object.values(result.cards)) {
        assignCard(card.cardId, card.subscriptionId);
      }
    },
    onError: (err) => showError(err.message),
    onCancel: () => {},
  });
</script>
📘

Both scripts can coexist

The two SDKs use separate globals and namespaced messages, so a page that ends up loading both tags during a staged rollout is fine. Don't mount both into the same container.

Rework the callbacks

onSuccess: one session result, at close

The legacy SDK fired onSuccess the moment a card enrolled, with that one card. The unified SDK fires it once, when the session closes with at least one completed action, with a map of everything the session created. Iterate the map instead of reading data.cardId:

onSuccess: (result) => {
  for (const card of Object.values(result.cards)) {
    assignCard(card.cardId, card.subscriptionId);
  }
},

Three things to check in your handler:

  • Timing. If you acted at the moment of enrollment (advancing your own UI, polling the subscription), read the card-enrolled event on onEvent; it still fires per card, as it happens.
  • Fields. enrollmentGuidance and authenticationFlow are not in the map. Capture them from the card-enrolled event if you use them.
  • Multiple cards. The success screen offers Link another card, so one session can produce several. The loop above handles that; pass linkAnotherCard: false to keep one-card sessions.
🚧

Webhooks stay your system of record

Session callbacks mirror this browser session. The durable record is the
cardsubscription.created webhook, which fires for every enrolled card regardless of how the
session ended. See Listen for results.

onError: new payload, and the flow continues

Legacy fieldUnified field
error.errorCodecode
error.detailmessage
type ("client" / "server")flow ("card" / "bank" / "feed" / "session")
verificationId, cardId, subscriptionIdUnchanged: present once the objects exist

Errors no longer end the enrollment. The SDK shows the cardholder the error screen and lets them retry or pick another path, so treat onError as a signal for logging and support, and let onSuccess or onCancel tell you how the session actually ended.

If you intercepted card_subscription.card_must_be_network_bulk_enrolled and called closeForm() to run your own flow, retire that workaround: enable the bulk rail and the SDK routes those cards to the bulk feed wizard itself.

onCancel: means nothing was enrolled

It fires only when the session closes with zero completed actions. A session that enrolled a card and then closed fires onSuccess, never onCancel. If your handler assumed no card was created, that assumption now always holds. If you only need to know the surface closed, listen for the closed event on onEvent; it fires on every close and carries the same maps onSuccess gets.

Optional: switch to onEvent

The callbacks above are enough for a like-for-like migration. onEvent is the unified SDK's primary callback and streams every action as it happens (card-enrolled, bank-linked, feed-created, error, closed, cancelled). Adopt it when you want per-action reactions, and before you turn on the bank or bulk rails. See Listen for results.

Update the types

Same package, new entry point. The root export keeps typing the legacy SDK, so both imports coexist while you migrate:

npm install --save-dev @astrada.co/card-enrollment-sdk@latest
// Before
import type { CardEnrollmentSdkConfig, EnrollmentSuccess } from "@astrada.co/card-enrollment-sdk";

// After
import type {
  UnifiedEnrollmentSdkConfig,
  EnrollmentSuccess, // now the consolidated session result
  SdkEvent,          // the onEvent union
} from "@astrada.co/card-enrollment-sdk/unified";

EnrollmentSuccess exists at both entry points with different shapes. Import it from the entry point that matches the SDK you're calling.

Migration checklist

  • Swap the script URL and its data-id.
  • Rename calls: CardEnrollmentSdk.openForm() to UnifiedEnrollmentSdk.open(), closeForm() to close().
  • Rework onSuccess to iterate result.cards; read authenticationFlow and enrollmentGuidance from the card-enrolled event if you use them.
  • Rework onError to read code and message, and stop treating an error as the end of the session.
  • Re-check onCancel handlers; it now fires only when nothing was enrolled.
  • Retire any closeForm() workaround for bulk-eligible cards.
  • Point TypeScript imports at @astrada.co/card-enrollment-sdk/unified.
  • Keep setStyle as is (same variables); add the new ones when you want them.
  • Test the full flow in sandbox before switching production traffic: Test Cards & Sandbox Testing.
  • When you're ready for bank linking or bulk feeds, set enrollmentPolicy on the subaccount: Choose your rails. No client release needed.

Related


Did this page help you?