Skip to content

Save a Card ​

Save a card for later payments without charging it, by confirming a setup intent your backend created. Setup intents only support direct confirmation — there is no token-based flow for them.

To charge the saved card later, see Save Card & Recurring.

Full example

Browser and server code for this flow: Integration Examples → Save a Card.

Example ​

typescript
// client_secret comes from your backend (create a setup intent server-side)
const { setup_intent, three_ds_payload } = await bahn.confirmCardSetup({
  client_secret,
});

if (three_ds_payload) {
  bahn.mount3DS(three_ds_payload.id, {
    onSuccess: () => {
      bahn.pollSetupIntentStatus(client_secret, {
        onSuccess: (setupIntent) => redirectToSuccess(),
        onFailure: (setupIntent) => redirectToFailure(),
        onTimeout: () => showTimeoutMessage(),
        onError: (error) => console.error("Poll error:", error.message),
      });
    },
    onFailure: (error) => showError(error?.message),
  });
} else if (setup_intent.status === "succeeded") {
  redirectToSuccess();
} else if (["failed", "canceled", "requires_payment_method"].includes(setup_intent.status)) {
  redirectToFailure();
} else {
  // Not final yet — poll until it is
  bahn.pollSetupIntentStatus(client_secret, {
    onSuccess: () => redirectToSuccess(),
    onFailure: () => redirectToFailure(),
    onTimeout: () => showTimeoutMessage(),
  });
}

How it works ​

sequenceDiagram
    participant App as Your App
    participant SDK as Bahn SDK
    participant CardIframe as Card Iframe
    participant API as Cake API

    App->>SDK: mountCard("#container", options)
    App->>SDK: confirmCardSetup(payload)
    SDK->>CardIframe: Delegate + merge card data
    CardIframe->>API: POST /api/sdk/setup_intents/confirm
    API-->>CardIframe: SetupIntent + optional base64 3DS payload
    CardIframe-->>SDK: Decoded result (no card data)
    SDK-->>App: { setup_intent, three_ds_payload? }

    opt three_ds_payload present
        App->>SDK: mount3DS(three_ds_payload.id, callbacks)
        App->>SDK: pollSetupIntentStatus(client_secret, callbacks)
    end

bahn.confirmCardSetup(payload) ​

Confirms a setup intent with the card in the mounted card fields. The request is sent from inside the card-number iframe, same as confirmPayment().

typescript
confirmCardSetup(payload: Bahn.ConfirmCardSetupPayload): Promise<Bahn.ConfirmCardSetupResult>;

Parameters — ConfirmCardSetupPayload:

FieldTypeRequiredDescription
client_secretstringYesClient secret of the setup intent your backend created
payment_method.billing_detailsConfirmationTokenBillingDetailsNoShopper's name, email, phone and address. Card data is added by the iframe
payment_method_optionsRecord<string, unknown>NoPayment-method-specific options

Returns — ConfirmCardSetupResult:

FieldTypeDescription
setup_intentRecord<string, unknown>The SetupIntent object
three_ds_payloadThreeDSPayloadDecoded | undefinedPresent only when 3DS authentication is required

Rejects with APIError — mountCard() must be called first. See Confirm Payment for the error cases, which are the same.

bahn.pollSetupIntentStatus(clientSecret, callbacks) / bahn.stopSetupIntentPolling() ​

Setup intent equivalents of pollPaymentIntentStatus()/stopPaymentIntentPolling(), with the same PaymentIntentPollCallbacks and status semantics — each callback receives the setup_intent instead. Max attempts is shared with the payment-intent poller (paymentIntentPollAttempts in BahnOptions).

typescript
pollSetupIntentStatus(clientSecret: string, callbacks: Bahn.PaymentIntentPollCallbacks): void;
stopSetupIntentPolling(): void;

Developers