> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paymentkit.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server.

# One-click saved cards

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](/guides/integration/sdk-reference/payment-kit-js/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](#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.

```bash
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).

> **Info**
>
> 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](/guides/integration/sdk-reference/payment-kit-js/card-payments):

```typescript
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.

```typescript
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.

```typescript
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.
}
```

| Option          | Type                           | Description                                                                                                                  |
| --------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `cmpCardId`     | `string`                       | The selected card's `cmpCardId`, so the CVC is re-collected against the right card. Re-mount when the selected card changes. |
| `placeholder`   | `string`                       | Placeholder text for the input                                                                                               |
| `style`         | `Record<string, string>`       | CSS properties applied to the input inside the iframe                                                                        |
| `onLoaded`      | `() => void`                   | Called when the field is ready to accept input                                                                               |
| `onFocusChange` | `(isFocused: boolean) => void` | Called when the input gains or loses focus                                                                                   |

> **Warning**
>
> **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.

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

```typescript
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](/guides/integration/sdk-reference/payment-kit-js/card-payments#3d-secure).

## 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()`:

```typescript
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`.

> **Note**
>
> 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.