2025, Nov 30 05:00

How to Save a Customer's Card in Stripe Checkout for Off-Session, One-Click Follow-Up Charges

Learn why Stripe Checkout sessions don't save cards and how setup_future_usage enables off-session backend charges with a reusable PaymentMethod - clear guide.

Charging a returning customer with Stripe after a Checkout Session often looks straightforward, until you realize the Customer ends up with no reusable card on file. The first payment succeeds, but the next server-side charge fails because there’s no PaymentMethod attached to the Customer. Here’s how to wire the flow correctly so you can do a true one-click follow-up payment without showing the Stripe UI again.

Problem statement

After the initial Checkout Session, the Customer object shows no payment_method, listing customer’s payment methods returns nothing, and attempting to reuse the PaymentMethod from the first charge triggers an error about reusing a PaymentMethod. The goal is a second one-off charge from the backend only, with no additional card entry.

Repro code

The backend creates a Checkout Session, then validates it and stores the customer_id. For the extra charge it tries to reuse a previous PaymentMethod:

@api_view(['POST'])
def begin_checkout(request):
    checkout_sess = BillingHub().checkout.Session.create(
        success_url=f'{settings.STRIPE_SUCCESS_URL}?session_id={{CHECKOUT_SESSION_ID}}',
        line_items=[{'price': 'price_XXXX', 'quantity': 1}],
        mode='payment',
    )
    return Response({'success_url': checkout_sess['url']}, status=200)

@api_view(['GET'])
def verify_checkout(request):
    sid = request.GET.get('session_id')
    chk = BillingHub().checkout.Session.retrieve(sid)
    # persist chk.customer and email

# later, try an extra one-off charge
charge_pi = stripe.PaymentIntent.create(
    amount=2000,
    currency='mxn',
    customer=stored_customer_id,
    payment_method=old_pm_id,  # attempt to reuse from the first charge
    off_session=True,
    confirm=True,
    description="Extra advice - one-off charge",
)

Why it fails

The Checkout Session was created without instructing Stripe to save the card for future use. As a result, the Customer ends up with no attached PaymentMethod. Listing payment methods returns empty because none were saved. The PaymentMethod used for the first PaymentIntent cannot be reused if it was never attached to the Customer, which explains the error about reusing it.

Solution

When creating the initial Checkout Session, explicitly ask Stripe to save the card onto the Customer by setting payment_intent_data[setup_future_usage]. With this configuration, Stripe will attach a PaymentMethod (for example, pm_1234) to the Customer upon successful payment. You can then read the PaymentMethod id from the PaymentIntent associated with that Checkout Session and store it server-side for later charges.

Fixed implementation

First, create the Checkout Session with setup_future_usage. Then, after success, retrieve the PaymentIntent and extract its payment_method to store it alongside the customer.

@api_view(['POST'])
def start_session(request):
    cs = BillingHub().checkout.Session.create(
        success_url=f'{settings.STRIPE_SUCCESS_URL}?session_id={{CHECKOUT_SESSION_ID}}',
        line_items=[{'price': 'price_XXXX', 'quantity': 1}],
        mode='payment',
        payment_intent_data={
            'setup_future_usage': 'off_session',  # ensures the card is saved on the Customer
        },
    )
    return Response({'success_url': cs['url']}, status=200)

@api_view(['GET'])
def finalize_session(request):
    sess_id = request.GET.get('session_id')
    sess = BillingHub().checkout.Session.retrieve(sess_id)

    # persist sess.customer and sess.customer_details.email
    pi_id = sess['payment_intent']
    pi = stripe.PaymentIntent.retrieve(pi_id)
    saved_pm = pi['payment_method']

    # store saved_pm associated with sess.customer for future payments

# later, perform the extra one-off charge without UI
followup_pi = stripe.PaymentIntent.create(
    amount=2000,
    currency='mxn',
    customer=stored_customer_id,
    payment_method=saved_pm_id_from_db,
    off_session=True,
    confirm=True,
    description="Extra advice - one-off charge",
)

Why this matters

If you rely on Checkout for the initial charge but don’t ask Stripe to retain the card, you’ll block yourself from any seamless follow-up payments. Setting payment_intent_data[setup_future_usage] makes the Customer reusable in a safe and predictable way and gives you a canonical PaymentMethod id you can use for later backend-only payments.

Takeaways

Always decide at the first Checkout whether you need future charges. If you do, enable payment_intent_data[setup_future_usage] so Stripe attaches a PaymentMethod to the Customer. After the session completes, read the PaymentIntent’s payment_method and store it. With that in place, backend-created PaymentIntents for subsequent one-off charges work as intended without re-entering card details.