# Save a customer's payment method without making a payment

Learn how to save a payment method and charge it later.

# Checkout Sessions API


The Checkout Sessions API in [setup](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-mode) mode lets you save a customer’s payment details without an initial payment. This is helpful if you want to onboard customers now, set them up for payments, and charge them using the Payment Intents API in the future—when they’re offline.

Use this integration to set up recurring payments or to create one-time payments with a final amount determined later, often after the customer receives your service.

> #### Card-present transactions
> 
> Card-present transactions, such as [collecting card details through Stripe Terminal](https://docs.stripe.com/terminal/features/saving-payment-details/save-directly.md), use a different process for saving the payment method.

## Compliance

You’re responsible for your compliance with all applicable laws, regulations, and network rules when saving a customer’s payment details. These requirements generally apply if you want to save your customer’s payment method for future use, such as displaying a customer’s payment method to them in the checkout flow for a future purchase or charging them when they’re not actively using your website or app. Add terms to your website or app that state how you plan to save payment method details and allow customers to opt in.

When you save a payment method, you can only use it for the specific usage you’ve included in your terms. To charge a payment method when a customer is offline and save it as an option for future purchases, make sure that you explicitly collect consent from the customer for this specific use. For example, include a “Save my payment method for future use” checkbox to collect consent.

To charge a customer when they’re offline, make sure your terms include the following:

- The customer’s agreement to your initiating a payment or a series of payments on their behalf for specified transactions.
- The anticipated timing and frequency of payments (for example, if the charges are for scheduled installments, subscription payments, or unscheduled top-ups).
- How you determine the payment amount.
- Your cancellation policy, if the payment method is for a subscription service.

Make sure you keep a record of your customer’s written agreement to these terms.

> If you need to use manual server-side confirmation or your integration requires presenting payment methods separately, see our [alternative guide](https://docs.stripe.com/payments/save-and-reuse-cards-only.md).

## Set up Stripe [Server-side]

First, [create a Stripe account](https://dashboard.stripe.com/register) or [sign in](https://dashboard.stripe.com/login).

Coding agents should install the Stripe CLI (`npm i -g @stripe/cli`) and run the command `stripe sandbox create --help` to provision an anonymous Stripe sandbox with working API keys. No account registration required.

Use our official libraries to access the Stripe API from your application:

#### Ruby

```bash
# Available as a gem
sudo gem install stripe
```

```ruby
# If you use bundler, you can add this line to your Gemfile
gem 'stripe'
```

## Create a customer [Server-side]

To set up a payment method for future payments, you must attach it to an object that represents your customer. When your customer creates an account or has their first transaction with your business, create either a customer-configured [Account](https://docs.stripe.com/api/v2/core/accounts/create.md) object with the Accounts v2 API or a [Customer](https://docs.stripe.com/api/customers/create.md) object with the Customers API.

> #### Use the Accounts v2 API to represent customers
> 
> The Accounts v2 API is generally available for Connect users, and in public preview for other Stripe users. If you’re part of the Accounts v2 preview, you need to specify a [preview version](https://docs.stripe.com/api-v2-overview.md#sdk-and-api-versioning) in your code.
> 
> To join the Accounts v2 preview, go to [Account previews and features](https://dashboard.stripe.com/settings/previews) in your Dashboard and enable **Reusable payment methods for Global Payouts**.
> 
> For most use cases, we recommend [modeling your customers as customer-configured Account objects](https://docs.stripe.com/accounts-v2/use-accounts-as-customers.md) instead of using [Customer](https://docs.stripe.com/api/customers.md) objects.

#### Accounts v2

```curl
curl -X POST https://api.stripe.com/v2/core/accounts \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-09-30.preview"
```

#### Customers v1

```curl
curl -X POST https://api.stripe.com/v1/customers \
  -u "<<YOUR_SECRET_KEY>>:"
```

## Use setup mode [Server-side]

Add an endpoint on your server that creates a Checkout Session with [mode=setup](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-mode) and returns its [client_secret](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-client_secret) to your front end. The client-side examples use `/create-checkout-session` and expect a JSON response with a `client_secret` property.

Set [return_url](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-return_url) to the page where you want to send customers after they save their payment method.

#### Accounts v2

Pass the ID of the [Account](https://docs.stripe.com/api/v2/core/accounts/create.md) you created in the previous step. After the Checkout Session completes successfully, Stripe automatically attaches the resulting payment method to that Account.

```curl
curl https://api.stripe.com/v1/checkout/sessions \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer_account={{CUSTOMERACCOUNT_ID}}" \
  -d mode=setup \
  -d ui_mode=elements \
  -d currency=usd \
  --data-urlencode "return_url=https://example.com/return?session_id={CHECKOUT_SESSION_ID}"
```

#### Customers v1

Pass the ID of the [Customer](https://docs.stripe.com/api/customers/create.md) you created in the previous step. After the Checkout Session completes successfully, Stripe automatically attaches the resulting payment method to that Customer.

```curl
curl https://api.stripe.com/v1/checkout/sessions \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer={{CUSTOMER_ID}}" \
  -d mode=setup \
  -d ui_mode=elements \
  -d currency=usd \
  --data-urlencode "return_url=https://example.com/return?session_id={CHECKOUT_SESSION_ID}"
```

## Set up the front end [Client-side]

#### HTML + JS

Include the Stripe.js script on your checkout page by adding it to the `head` of your HTML file. Always load Stripe.js directly from js.stripe.com to remain PCI compliant. Don’t include the script in a bundle or host a copy of it yourself.

Make sure you’re using the latest Stripe.js version. Learn more about [Stripe.js versioning](https://docs.stripe.com/sdks/stripejs-versioning.md).

```html
<head>
  <title>Save a payment method</title>
  <script src="https://js.stripe.com/endive/stripe.js"></script>
  <script type="module" src="checkout.js"></script>
</head>
```

Initialize Stripe.js with your publishable key. Fetch the Checkout Session’s client secret from your server, then pass it to [initCheckoutElementsSdk](https://docs.stripe.com/js/custom_checkout/init).

```javascript
const stripe = Stripe('<<YOUR_PUBLISHABLE_KEY>>');

async function fetchClientSecret() {
  const response = await fetch('/create-checkout-session', {method: 'POST'});
  const {client_secret} = await response.json();
  return client_secret;
}

const clientSecret = fetchClientSecret();
const checkout = stripe.initCheckoutElementsSdk({clientSecret});
```

#### React

Install the latest versions of [React Stripe.js](https://www.npmjs.com/package/@stripe/react-stripe-js) and the [Stripe.js loader](https://www.npmjs.com/package/@stripe/stripe-js) from the npm public registry.

```bash
npm install --save @stripe/react-stripe-js @stripe/stripe-js
```

Call `loadStripe` with your publishable key outside your component to avoid recreating the `Stripe` object on each render. Fetch the Checkout Session’s client secret from your server.

Wrap your application with the [CheckoutElementsProvider](https://docs.stripe.com/js/react_stripe_js/checkout/checkout_provider) component, passing in `clientSecret` and the `stripe` instance.

```jsx
import React from 'react';
import {loadStripe} from '@stripe/stripe-js';
import {CheckoutElementsProvider} from '@stripe/react-stripe-js/checkout';
import SetupForm from './SetupForm';

const stripe = loadStripe('<<YOUR_PUBLISHABLE_KEY>>');

async function fetchClientSecret() {
  const response = await fetch('/create-checkout-session', {method: 'POST'});
  const {client_secret} = await response.json();
  return client_secret;
}

const clientSecret = fetchClientSecret();

export default function App() {
  return (
    <CheckoutElementsProvider stripe={stripe} options={{clientSecret}}>
      <SetupForm />
    </CheckoutElementsProvider>
  );
}
```

## Collect payment details [Client-side]

Collect payment details on the client with the [Payment Element](https://docs.stripe.com/payments/payment-element.md). The Payment Element is a prebuilt UI component that simplifies collecting payment details for a variety of payment methods.

You must provide a valid customer email when completing a Checkout Session. Use the [Contact Details Element](https://docs.stripe.com/js/custom_checkout/create_contact_details_element) to collect your customer’s email address. It handles email collection and validation for you.

The Payment Element contains an iframe that securely sends payment information to Stripe over an HTTPS connection. Avoid placing the Payment Element within another iframe because some payment methods require redirecting to another page for payment confirmation.

If you choose to use an iframe and want to accept Apple Pay or Google Pay, the iframe must have the [allow](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allowpaymentrequest) attribute set to equal `"payment *"`.

The checkout page address must start with `https://` rather than `http://` for your integration to work. You can test your integration without using HTTPS, but remember to [enable it](https://docs.stripe.com/security/guide.md#tls) when you’re ready to accept live payments.

#### HTML + JS

Add a form with containers for the Elements and a **Save payment method** button. Keep the button disabled until Checkout is ready to confirm the setup.

```html
<form id="setup-form">
  <div id="contact-details-element"></div>
  <div id="payment-element"></div>
  <button id="save-button" type="submit" disabled>Save payment method</button>
  <div id="error-message" role="alert"></div>
</form>
```

Create and mount both Elements using the `checkout` instance from the previous step.

```javascript
const contactDetailsElement = checkout.createContactDetailsElement();
contactDetailsElement.mount('#contact-details-element');

const paymentElement = checkout.createPaymentElement();
paymentElement.mount('#payment-element');
```

#### React

Mount the `ContactDetailsElement` and `PaymentElement` components within the `CheckoutElementsProvider`. Access the `Checkout` object in your form component using the [useCheckoutElements](https://docs.stripe.com/js/react_stripe_js/checkout/use_checkout_elements) hook, and wait for Checkout to finish loading before mounting the Elements.

```jsx
import React from 'react';
import {
  ContactDetailsElement,
  PaymentElement,
  useCheckoutElements,
} from '@stripe/react-stripe-js/checkout';

export default function SetupForm() {
  const checkoutState = useCheckoutElements();

  if (checkoutState.type === 'loading') {
    return <div>Loading...</div>;
  }
  if (checkoutState.type === 'error') {
    return <div role="alert">{checkoutState.error.message}</div>;
  }

  return (
    <form>
      <ContactDetailsElement />
      <PaymentElement />
      <button type="submit" disabled>Save payment method</button>
    </form>
  );
}
```

## Save the payment method [Client-side]

Call [confirm](https://docs.stripe.com/js/custom_checkout/confirm) to complete the setup using details collected by the Payment Element. For a Session in `setup` mode, this saves the payment method without charging the customer. If additional authentication is required, Checkout handles it before redirecting your customer to the `return_url` you set when creating the Session. Display any immediate errors so your customer can correct their details and try again.

#### HTML + JS

Call `loadActions` on the existing `checkout` instance to access `confirm`. Use the Session’s `canConfirm` property to enable the button, and disable it while a submission is in progress.

```javascript
async function initializeForm() {
  const form = document.getElementById('setup-form');
  const button = document.getElementById('save-button');
  const errors = document.getElementById('error-message');
  const loadActionsResult = await checkout.loadActions();

  if (loadActionsResult.type === 'error') {
    errors.textContent = loadActionsResult.error.message;
    return;
  }

  const {actions} = loadActionsResult;
  let submitting = false;

  button.disabled = !actions.getSession().canConfirm;
  checkout.on('change', (session) => {
    button.disabled = submitting || !session.canConfirm;
  });

  form.addEventListener('submit', async (event) => {
    event.preventDefault();
    if (submitting || !actions.getSession().canConfirm) {
      return;
    }

    submitting = true;
    button.disabled = true;
    errors.textContent = '';

    const result = await actions.confirm();
    if (result.type === 'error') {
      errors.textContent = result.error.message;
      submitting = false;
      button.disabled = !actions.getSession().canConfirm;
    }
  });
}

await initializeForm();
```

#### React

Update `SetupForm` to call `checkout.confirm()` on submission. Use `checkout.canConfirm` and your submission state to control the button.

```jsx
import React, {useState} from 'react';
import {
  ContactDetailsElement,
  PaymentElement,
  useCheckoutElements,
} from '@stripe/react-stripe-js/checkout';

export default function SetupForm() {
  const checkoutState = useCheckoutElements();
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState(null);

  if (checkoutState.type === 'loading') {
    return <div>Loading...</div>;
  }
  if (checkoutState.type === 'error') {
    return <div role="alert">{checkoutState.error.message}</div>;
  }

  const {checkout} = checkoutState;

  const handleSubmit = async (event) => {
    event.preventDefault();
    if (submitting || !checkout.canConfirm) {
      return;
    }

    setSubmitting(true);
    setError(null);

    const result = await checkout.confirm();
    if (result.type === 'error') {
      setError(result.error.message);
      setSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <ContactDetailsElement />
      <PaymentElement />
      <button type="submit" disabled={submitting || !checkout.canConfirm}>
        Save payment method
      </button>
      {error && <div role="alert">{error}</div>}
    </form>
  );
}
```

## Attach the payment method [Server-side]

> If you passed a customer or account when [creating the Checkout Session](https://docs.stripe.com/payments/save-and-reuse.md#use-setup-mode), the payment method is already attached after the Session completes. Skip to [Charge the payment method later](https://docs.stripe.com/payments/save-and-reuse.md#charge-saved-payment-method).

If you didn’t pass a customer or account when creating the Checkout Session, retrieve the payment method ID from the [checkout.session.completed](https://docs.stripe.com/api/events/types.md#event_types-checkout.session.completed) webhook and attach it manually:

1. Get the [setup_intent](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-setup_intent) value from the completed Session object.
2. [Retrieve](https://docs.stripe.com/api/setup_intents/retrieve.md) the SetupIntent to get the [payment_method](https://docs.stripe.com/api/setup_intents/object.md#setup_intent_object-payment_method) ID.
3. Use that ID to attach the payment method:

#### Accounts v2

```curl
curl https://api.stripe.com/v1/payment_methods/{{PAYMENTMETHOD_ID}}/attach \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer_account={{CUSTOMERACCOUNT_ID}}"
```

#### Customers v1

```curl
curl https://api.stripe.com/v1/payment_methods/{{PAYMENTMETHOD_ID}}/attach \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer={{CUSTOMER_ID}}"
```

Learn more about [setting up webhooks](https://docs.stripe.com/webhooks.md).

## Charge the payment method later [Server-side]

To charge the customer later, you need the ID of the `PaymentMethod` saved to them. Retrieve it by listing their saved payment methods, or use the ID you stored when handling the [checkout.session.completed](https://docs.stripe.com/api/events/types.md#event_types-checkout.session.completed) webhook:

#### Accounts v2

```curl
curl -G https://api.stripe.com/v1/payment_methods \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer_account={{CUSTOMERACCOUNT_ID}}"
```

#### Customers v1

```curl
curl https://api.stripe.com/v1/customers/{{CUSTOMER_ID}}/payment_methods \
  -u "<<YOUR_SECRET_KEY>>:"
```

The response includes a `data` array of [PaymentMethod](https://docs.stripe.com/api/payment_methods/object.md) objects.

After you have the PaymentMethod ID, you can make an *off-session* (A payment is described as off-session if it occurs without the direct involvement of the customer, using previously-collected payment information) payment using a [PaymentIntent](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-payment_method):

#### Accounts v2

- Set [customer_account](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-customer_account) to the Account ID, and set [payment_method](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-payment_method) to the PaymentMethod ID.

#### Customers v1

- Set [customer](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-customer) to the Customer ID, and set [payment_method](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-payment_method) to the PaymentMethod ID.

- Set [off_session](https://docs.stripe.com/api/payment_intents/confirm.md#confirm_payment_intent-off_session) to `true` to indicate that the customer isn’t in your checkout flow during a payment attempt and can’t fulfill an authentication request made by a partner, such as a card issuer, bank, or other payment institution. If, during your checkout flow, a partner requests authentication, Stripe requests exemptions using customer information from a previous *on-session* (A payment is described as on-session if it occurs while the customer is actively in your checkout flow and able to authenticate the payment method) transaction. If the conditions for exemption aren’t met, the PaymentIntent might result in an error.
- Set the value of the PaymentIntent’s [confirm](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-confirm) property to `true`, which causes confirmation to occur immediately when you create the PaymentIntent.

#### Accounts v2

```curl
curl https://api.stripe.com/v1/payment_intents \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d amount=1099 \
  -d currency=usd \
  -d "customer_account={{CUSTOMERACCOUNT_ID}}" \
  -d "payment_method={{PAYMENTMETHOD_ID}}" \
  -d off_session=true \
  -d confirm=true
```

#### Customers v1

```curl
curl https://api.stripe.com/v1/payment_intents \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d amount=1099 \
  -d currency=usd \
  -d "customer={{CUSTOMER_ID}}" \
  -d "payment_method={{PAYMENTMETHOD_ID}}" \
  -d off_session=true \
  -d confirm=true
```

If a payment attempt fails, the request also fails with a 402 HTTP status code, and the PaymentIntent status is *requires\_payment\_method* (This status appears as "requires_source" in API versions before 2019-02-11). Notify your customer to return to your application (for example, by sending an email or in-app notification) and direct your customer to a new Checkout Session to select another payment method.

#### Accounts v2

```curl
curl https://api.stripe.com/v1/checkout/sessions \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer_account={{CUSTOMERACCOUNT_ID}}" \
  -d "line_items[0][price_data][currency]=usd" \
  -d "line_items[0][price_data][product_data][name]=T-shirt" \
  -d "line_items[0][price_data][unit_amount]=1099" \
  -d "line_items[0][quantity]=1" \
  -d mode=payment \
  -d ui_mode=elements \
  --data-urlencode "return_url=https://example.com/return"
```

#### Customers v1

```curl
curl https://api.stripe.com/v1/checkout/sessions \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d "customer={{CUSTOMER_ID}}" \
  -d "line_items[0][price_data][currency]=usd" \
  -d "line_items[0][price_data][product_data][name]=T-shirt" \
  -d "line_items[0][price_data][unit_amount]=1099" \
  -d "line_items[0][quantity]=1" \
  -d mode=payment \
  -d ui_mode=elements \
  --data-urlencode "return_url=https://example.com/return"
```

