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
Save the payment method during initial checkout
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.
1// Option 1: Save during Checkout2const 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});1213// 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.
List saved payment methods
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.
1const paymentMethods = await stripe.paymentMethods.list({2 customer: customerId,3 type: 'card',4});56console.log(paymentMethods.data);7// Each entry has: id, card.brand, card.last4, card.exp_month, card.exp_yearExpected result: An array of saved payment methods with card details (brand, last4, expiry).
Charge the saved card off-session
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.
1async function chargeSavedCard(customerId, paymentMethodId, amount) {2 try {3 const paymentIntent = await stripe.paymentIntents.create({4 amount: amount, // in cents5 currency: 'usd',6 customer: customerId,7 payment_method: paymentMethodId,8 off_session: true,9 confirm: true,10 });1112 return { success: true, paymentIntent };13 } catch (err) {14 if (err.code === 'authentication_required') {15 // Card requires 3D Secure — notify customer to authenticate16 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.
Handle authentication-required cases
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.
1// Send customer to an authentication page with the PaymentIntent ID2// 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
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23async 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 });1617 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 }2930 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 }3435 console.error('Unexpected error:', err.message);36 throw err;37 }38}3940async 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}5354module.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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation