Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to charge a saved card using Stripe

Charge a saved card by creating a PaymentIntent with the customer ID and payment_method ID, setting off_session to true and confirm to true. Stripe attempts the charge without customer interaction. Handle 'requires_action' for cards that need 3D Secure by notifying the customer to return and authenticate.

What you'll learn

  • How to save a customer's payment method for future use
  • How to charge a saved card off-session without customer interaction
  • How to handle authentication requirements on saved cards
  • How to list a customer's saved payment methods
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner5 min read15 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

Charge a saved card by creating a PaymentIntent with the customer ID and payment_method ID, setting off_session to true and confirm to true. Stripe attempts the charge without customer interaction. Handle 'requires_action' for cards that need 3D Secure by notifying the customer to return and authenticate.

Reusing Payment Methods for Repeat Charges

Many businesses need to charge customers without them being present — reorders, usage-based billing, or manual invoices. Stripe lets you save a customer's payment method during their first purchase, then charge it later by referencing the customer and payment_method IDs. The key is setting off_session: true so Stripe knows the customer is not actively on your site, and using the right error handling for cards that require authentication.

Prerequisites

  • A Stripe account with test API keys
  • Node.js 18+ with the stripe npm package installed
  • A Stripe Customer already created (stripe.customers.create())
  • A saved PaymentMethod attached to that customer

Step-by-step guide

1

Save the payment method during initial checkout

When the customer first pays, use a Checkout Session or SetupIntent with setup_future_usage or mode: 'setup' to save their card. This stores the payment method for later use.

typescript
1// Option 1: Save during Checkout
2const session = await stripe.checkout.sessions.create({
3 mode: 'payment',
4 customer: customerId,
5 payment_intent_data: {
6 setup_future_usage: 'off_session',
7 },
8 line_items: [{ price: 'price_xxx', quantity: 1 }],
9 success_url: 'https://yoursite.com/success',
10 cancel_url: 'https://yoursite.com/cancel',
11});
12
13// Option 2: Save via SetupIntent (no charge)
14const setupIntent = await stripe.setupIntents.create({
15 customer: customerId,
16 automatic_payment_methods: { enabled: true },
17});

Expected result: The payment method is saved and attached to the customer. You can see it in Dashboard → Customers → [Customer] → Payment methods.

2

List saved payment methods

Before charging, you may want to list the customer's saved payment methods to let them pick one or to grab the default.

typescript
1const paymentMethods = await stripe.paymentMethods.list({
2 customer: customerId,
3 type: 'card',
4});
5
6console.log(paymentMethods.data);
7// Each entry has: id, card.brand, card.last4, card.exp_month, card.exp_year

Expected result: An array of saved payment methods with card details (brand, last4, expiry).

3

Charge the saved card off-session

Create a PaymentIntent with the customer ID, payment method ID, off_session: true, and confirm: true. Stripe attempts the charge immediately without the customer present.

typescript
1async function chargeSavedCard(customerId, paymentMethodId, amount) {
2 try {
3 const paymentIntent = await stripe.paymentIntents.create({
4 amount: amount, // in cents
5 currency: 'usd',
6 customer: customerId,
7 payment_method: paymentMethodId,
8 off_session: true,
9 confirm: true,
10 });
11
12 return { success: true, paymentIntent };
13 } catch (err) {
14 if (err.code === 'authentication_required') {
15 // Card requires 3D Secure — notify customer to authenticate
16 return {
17 success: false,
18 requiresAuth: true,
19 paymentIntentId: err.raw.payment_intent.id,
20 };
21 }
22 throw err;
23 }
24}

Expected result: For most saved cards, the PaymentIntent immediately transitions to 'succeeded'. For cards requiring auth, you get an authentication_required error.

4

Handle authentication-required cases

When a saved card requires 3D Secure, send the customer an email or notification with a link to complete authentication. On that page, use stripe.confirmPayment() to let them authenticate.

typescript
1// Send customer to an authentication page with the PaymentIntent ID
2// On the authentication page:
3const { error } = await stripe.confirmPayment({
4 clientSecret: paymentIntent.client_secret,
5 confirmParams: {
6 return_url: 'https://yoursite.com/payment-authenticated',
7 },
8});

Expected result: The customer completes 3D Secure and the payment succeeds.

Complete working example

charge-saved-card.js
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3async function chargeSavedCard(customerId, paymentMethodId, amountInCents) {
4 try {
5 const paymentIntent = await stripe.paymentIntents.create({
6 amount: amountInCents,
7 currency: 'usd',
8 customer: customerId,
9 payment_method: paymentMethodId,
10 off_session: true,
11 confirm: true,
12 metadata: {
13 charge_type: 'saved_card_recharge',
14 },
15 });
16
17 console.log(`Payment succeeded: ${paymentIntent.id}`);
18 return { success: true, paymentIntentId: paymentIntent.id };
19 } catch (err) {
20 if (err.code === 'authentication_required') {
21 console.log('Authentication required — notify customer');
22 return {
23 success: false,
24 requiresAuth: true,
25 paymentIntentId: err.raw.payment_intent.id,
26 clientSecret: err.raw.payment_intent.client_secret,
27 };
28 }
29
30 if (err.code === 'card_declined') {
31 console.log('Card declined — ask customer to update payment method');
32 return { success: false, declined: true, message: err.message };
33 }
34
35 console.error('Unexpected error:', err.message);
36 throw err;
37 }
38}
39
40async function listSavedCards(customerId) {
41 const methods = await stripe.paymentMethods.list({
42 customer: customerId,
43 type: 'card',
44 });
45 return methods.data.map((pm) => ({
46 id: pm.id,
47 brand: pm.card.brand,
48 last4: pm.card.last4,
49 expMonth: pm.card.exp_month,
50 expYear: pm.card.exp_year,
51 }));
52}
53
54module.exports = { chargeSavedCard, listSavedCards };

Common mistakes when charging a saved card using Stripe

Why it's a problem: Forgetting to set off_session: true for server-initiated charges

How to avoid: Without off_session: true, Stripe may require authentication which cannot happen without the customer present. Always set this flag for saved-card charges.

Why it's a problem: Not saving the payment method during initial checkout

How to avoid: Use setup_future_usage: 'off_session' on the PaymentIntent or Checkout Session during the first payment. Without this, the payment method is not reusable.

Why it's a problem: Not handling the authentication_required error

How to avoid: Some cards require 3D Secure even for saved cards. Catch this error and send the customer a link to complete authentication.

Why it's a problem: Storing card numbers instead of using Stripe PaymentMethod IDs

How to avoid: Never store raw card numbers. Save the Stripe PaymentMethod ID (pm_xxx) and Customer ID (cus_xxx). Stripe handles PCI compliance for you.

Best practices

  • Use setup_future_usage: 'off_session' during the initial payment to optimize for future off-session charges
  • Always handle authentication_required errors — send the customer a notification to authenticate
  • Store PaymentMethod IDs (pm_xxx) in your database, never raw card numbers
  • Test off-session charges with card 4242 4242 4242 4242 (succeeds) and 4000 0025 0000 3155 (requires auth)
  • Set up webhooks for payment_intent.succeeded and payment_intent.payment_failed to track results
  • Display saved cards to the customer with brand and last4 digits so they can choose which to charge
  • Handle card_declined errors gracefully — prompt the customer to update their payment method

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js function that charges a saved Stripe payment method off-session. Accept customerId, paymentMethodId, and amount as parameters. Handle authentication_required and card_declined errors separately.

Stripe Prompt

Add a 'charge saved card' feature. Create an endpoint that takes a customer ID and payment method ID, charges the saved card off-session using Stripe PaymentIntents, and handles authentication_required errors by returning a client_secret for the customer to complete authentication.

Frequently asked questions

Can I charge a saved card without the customer being on my site?

Yes. Set off_session: true and confirm: true when creating the PaymentIntent. Stripe charges the card immediately. However, some cards may require authentication — handle this by notifying the customer.

How long does a saved payment method last?

A saved PaymentMethod stays valid until the card expires or the customer removes it. Stripe automatically updates cards through its Account Updater service when banks issue new card numbers.

What happens if the saved card is expired?

The charge fails with a card_declined error code. Notify the customer to update their payment method. Stripe's Account Updater often prevents this by auto-updating card details.

Do I need to be PCI compliant to save cards?

No. Stripe handles card storage and PCI compliance. You only store the PaymentMethod ID (pm_xxx), which is a token — not the actual card number.

Can RapidDev help me build a saved-card payment system?

Yes. RapidDev can architect a complete saved-card system including the initial card-saving flow, recurring charge logic, failed-payment retry strategies, and customer payment-method management UI.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.