Skip to navigation

One-click saved cards

Let a returning customer pay in one click with a card they saved earlier.
View as Markdown

One-click lets a returning customer pay with a card they saved on an earlier visit — a single confirmation, no retyping the card, all inside your PaymentKit.js checkout. It builds on the Card payments flow, so start there if you don’t have a card checkout working yet.

PaymentKit.js gives you three things: the customer’s saved-card list, an optional CVC re-entry field, and the confirm call. You build the card picker UI yourself — there is no prebuilt picker element.

Before you begin

  • Your account routes to Stripe (the only processor that supports one-click saved-card charges today — see Limitations).
  • The checkout session is created with a customer_id — one-click is never anonymous.

Create the checkout session

Create the session server-side with your secret key. Use mode: "payment" and a customer_id, then hand the returned secure_token to PaymentKit.js in the browser.

curl -X POST https://app.paymentkit.com/api/{account_id}/checkout-sessions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"mode": "payment",
"customer_id": "cus_abc123",
"line_items": [
{ "price_id": "price_xyz", "quantity": 1 }
],
"default_payment_method_id": "pm_abc123",
"saved_card_cvc_reprompt": true,
"success_url": "https://yoursite.com/success",
"return_url": "https://yoursite.com/cancel"
}'
  • default_payment_method_id — optional. Suggests which saved card to preselect. It’s a prefill hint, not a lock; the customer can still pick another card, and the confirm call decides what is charged.
  • saved_card_cvc_reprompt — optional, set per session at create time (there is no account-level setting). true asks the customer to re-enter their security code, which generally improves approval rates. Omitted, it defaults to false (pure one-click, no CVC).

If you run your own backend, you can also list a customer’s saved cards server-side with your secret key. In the browser, use listSavedCards() (below) instead.

Charge the saved card in the browser

Initialize PaymentKit.js with the card payment method, exactly as in Card payments:

import PaymentKit from '@payment-kit-js/vanilla';
import CardPaymentMethod from '@payment-kit-js/vanilla/payment-methods/card';
const paymentKit = PaymentKit({
environment: 'sandbox',
secureToken: 'aBcDeFgHiJkLmNoPqRsTuVwXyZ123456', // secure_token from your backend
paymentMethods: [CardPaymentMethod]
});

List the saved cards

listSavedCards() returns the session customer’s saved cards. Preselect the one flagged isDefault (this reflects default_payment_method_id), or the first card. Render the picker with your own UI.

const result = await paymentKit.card.listSavedCards();
if ('errors' in result) {
// Couldn't load the cards — fall back to the normal card form.
showFreshCardForm();
} else {
const cards = result.data; // [{ id, brand, last4, expMonth, expYear, isDefault, cmpCardId }]
const selected = cards.find((c) => c.isDefault) ?? cards[0];
renderPicker(cards, selected); // your UI
}

If a customer has no saved cards, show your normal card form instead.

Re-prompt for the CVC when required

Read the session’s setting with getSavedCardCvcReprompt(). When it’s on, mount a CVC-only field for the selected card and disable the pay button until onLoaded fires — this stops a fast click from firing a CVC-less charge before the field is ready.

const repromptResult = await paymentKit.card.getSavedCardCvcReprompt();
// Fail safe: on an error, treat re-prompt as ON — never silently charge without a CVC.
const cvcRequired = 'errors' in repromptResult ? true : repromptResult.data;
if (cvcRequired) {
const payButton = document.querySelector('#pay-button');
payButton.disabled = true; // gate until the field is ready
const cvcElement = paymentKit.card.createSavedCardCvcElement({
cmpCardId: selected.cmpCardId, // re-collect the CVC against the chosen card
placeholder: 'CVC',
onLoaded: () => { payButton.disabled = false; }
});
const mountedCvc = cvcElement.mount('#saved-card-cvc');
// When the customer picks a different card, re-gate: mountedCvc.unmount(),
// then create and mount a new element with the new card's cmpCardId.
}
OptionTypeDescription
cmpCardIdstringThe selected card’s cmpCardId, so the CVC is re-collected against the right card. Re-mount when the selected card changes.
placeholderstringPlaceholder text for the input
styleRecord<string, string>CSS properties applied to the input inside the iframe
onLoaded() => voidCalled when the field is ready to accept input
onFocusChange(isFocused: boolean) => voidCalled when the input gains or loses focus

PaymentKit does not enforce CVC re-prompt on saved cards. If you don’t gate the charge on the CVC field (or collect a CVC another way), the charge is captured silently without a CVC, which lowers approval rates. Always disable the pay button until onLoaded fires, and if getSavedCardCvcReprompt() returns an error, treat re-prompt as on rather than firing a CVC-less charge.

Confirm the charge

Call confirmSavedCard() with the selected card’s id and cmpCardId, so a re-prompted CVC is re-collected against the right card.

confirmSavedCard() returns a Promise — it does not take the onSuccess / onError callbacks that submit() uses. Branch on the resolved result instead.

const result = await paymentKit.card.confirmSavedCard(selected.id, {
cmpCardId: selected.cmpCardId
});
if ('errors' in result) {
// errors.root — a customer-facing message.
// errors.card_cvc === 'invalid' — the CVC re-collection failed.
// errors.root === 'No saved card was selected' — no card id was passed.
showError(result.errors.root ?? 'Payment failed');
} else {
// result.data is the checkout response — the charge succeeded.
window.location.href = '/success';
}

3D Secure is handled inline, identically to a fresh card — no extra code. For decline details (errors.checkout_response) and 3DS requirements, see Card payments.

Let the customer use a different card

There’s no swap API — this is just your own UI state. To let the customer enter a fresh card, unmount the CVC element and render the ordinary card elements, then charge with submit():

mountedCvc.unmount();
const cardNumber = paymentKit.card.createElement('card_pan').mount('#card-number');
const cardExpiry = paymentKit.card.createElement('card_exp').mount('#card-expiry');
const cardCvc = paymentKit.card.createElement('card_cvc').mount('#card-cvc');
// ...then paymentKit.submit({ ... }) — see the Card payments guide.

Limitations

  • Stripe only today. A one-click saved-card charge routed to any other processor is rejected before any charge is attempted. Enable it on a Stripe route.
  • You build the picker. PaymentKit.js gives you the card list, the CVC field, and the confirm call — not a prebuilt picker component. Render the list yourself.
  • PaymentKit.js version. Requires @payment-kit-js/vanilla >= <VERSION> — the release that adds listSavedCards, getSavedCardCvcReprompt, createSavedCardCvcElement, and confirmSavedCard.

Replace <VERSION> with the published version before this page ships — check npm view @payment-kit-js/vanilla dist-tags. These methods are not in a released build yet.