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
// 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().
confirmCardSetup(payload: Bahn.ConfirmCardSetupPayload): Promise<Bahn.ConfirmCardSetupResult>;Parameters — ConfirmCardSetupPayload:
| Field | Type | Required | Description |
|---|---|---|---|
client_secret | string | Yes | Client secret of the setup intent your backend created |
payment_method.billing_details | ConfirmationTokenBillingDetails | No | Shopper's name, email, phone and address. Card data is added by the iframe |
payment_method_options | Record<string, unknown> | No | Payment-method-specific options |
Returns — ConfirmCardSetupResult:
| Field | Type | Description |
|---|---|---|
setup_intent | Record<string, unknown> | The SetupIntent object |
three_ds_payload | ThreeDSPayloadDecoded | undefined | Present 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).
pollSetupIntentStatus(clientSecret: string, callbacks: Bahn.PaymentIntentPollCallbacks): void;
stopSetupIntentPolling(): void;