/stripe-guides

How to charge a saved card using Stripe?

Learn how to securely charge a saved card using Stripe with step-by-step code examples, error handling, authentication, and best practices for seamless payments.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to charge a saved card using Stripe?

How to Charge a Saved Card Using Stripe: A Comprehensive Tutorial

 

Introduction

 

In this tutorial, I'll guide you through the process of charging a saved card using Stripe. This is a common requirement for subscription-based services, repeat purchases, or any business model where you need to charge customers without asking for their card details every time.

 

Prerequisites

 

  • A Stripe account (sign up at stripe.com if you don't have one)
  • Stripe API keys (available in your Stripe Dashboard)
  • A server environment with your preferred programming language
  • Basic understanding of RESTful APIs
  • Previously saved customer and card information in Stripe

 

Step 1: Set Up Your Stripe Environment

 

First, you need to install the Stripe library for your programming language. Here are examples for popular languages:

For Node.js:


npm install stripe

For Python:


pip install stripe

For PHP:


composer require stripe/stripe-php

For Ruby:


gem install stripe

 

Step 2: Initialize the Stripe Client

 

Now, let's initialize the Stripe client with your API key. Here's how to do it in different languages:

Node.js:


const stripe = require('stripe')('sk_test_your_secret_key');

Python:


import stripe
stripe.api_key = "sk_test_your_secret\_key"

PHP:


require\_once 'vendor/autoload.php';
\Stripe\Stripe::setApiKey('sk_test_your_secret_key');

Ruby:


require 'stripe'
Stripe.api_key = 'sk_test_your_secret\_key'

 

Step 3: Understand Stripe's Approach to Saved Cards

 

In Stripe, saved cards are associated with Customer objects. When you save a card, Stripe creates a PaymentMethod or Card object (depending on which API you're using) and attaches it to a Customer. To charge a saved card, you'll need:

  • The Customer ID
  • The PaymentMethod ID or Card ID

 

Step 4: Charge a Saved Card Using the PaymentIntents API (Recommended)

 

The PaymentIntents API is Stripe's recommended way to accept payments. Here's how to charge a saved card:

Node.js:


async function chargeCustomer(customerId, paymentMethodId, amount) {
  try {
    // Create a PaymentIntent
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount, // amount in cents
      currency: 'usd',
      customer: customerId,
      payment\_method: paymentMethodId,
      off\_session: true, // Important for saved cards
      confirm: true, // Confirm the payment immediately
    });
    
    console.log(`Payment successful: ${paymentIntent.id}`);
    return paymentIntent;
  } catch (error) {
    console.error('Error charging customer:', error);
    throw error;
  }
}

// Example usage
chargeCustomer('cus_AbCdEfGhIjKlMn', 'pm_1234567890', 2000); // $20.00

Python:


def charge_customer(customer_id, payment_method_id, amount):
    try:
        # Create a PaymentIntent
        payment\_intent = stripe.PaymentIntent.create(
            amount=amount,  # amount in cents
            currency='usd',
            customer=customer\_id,
            payment_method=payment_method\_id,
            off\_session=True,  # Important for saved cards
            confirm=True,  # Confirm the payment immediately
        )
        
        print(f"Payment successful: {payment\_intent.id}")
        return payment\_intent
    except Exception as e:
        print(f"Error charging customer: {e}")
        raise e

# Example usage
charge_customer('cus_AbCdEfGhIjKlMn', 'pm\_1234567890', 2000)  # $20.00

PHP:


function chargeCustomer($customerId, $paymentMethodId, $amount) {
    try {
        // Create a PaymentIntent
        $paymentIntent = \Stripe\PaymentIntent::create([
            'amount' => $amount, // amount in cents
            'currency' => 'usd',
            'customer' => $customerId,
            'payment\_method' => $paymentMethodId,
            'off\_session' => true, // Important for saved cards
            'confirm' => true, // Confirm the payment immediately
        ]);
        
        echo "Payment successful: " . $paymentIntent->id;
        return $paymentIntent;
    } catch (\Exception $e) {
        echo "Error charging customer: " . $e->getMessage();
        throw $e;
    }
}

// Example usage
chargeCustomer('cus_AbCdEfGhIjKlMn', 'pm_1234567890', 2000); // $20.00

Ruby:


def charge_customer(customer_id, payment_method_id, amount)
  begin
    # Create a PaymentIntent
    payment\_intent = Stripe::PaymentIntent.create({
      amount: amount, # amount in cents
      currency: 'usd',
      customer: customer\_id,
      payment_method: payment_method\_id,
      off\_session: true, # Important for saved cards
      confirm: true, # Confirm the payment immediately
    })
    
    puts "Payment successful: #{payment\_intent.id}"
    return payment\_intent
  rescue Stripe::StripeError => e
    puts "Error charging customer: #{e.message}"
    raise e
  end
end

# Example usage
charge_customer('cus_AbCdEfGhIjKlMn', 'pm\_1234567890', 2000) # $20.00

 

Step 5: Handle Authentication Requirements

 

Sometimes, even with saved cards, a payment might require additional authentication. This is especially common in regions with Strong Customer Authentication (SCA) requirements like Europe. Here's how to handle this scenario:

Node.js:


async function chargeCustomerWithAuthHandling(customerId, paymentMethodId, amount) {
  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: 'usd',
      customer: customerId,
      payment\_method: paymentMethodId,
      off\_session: true,
      confirm: true,
    });
    
    if (paymentIntent.status === 'requires\_action') {
      // The payment requires additional authentication
      return {
        requiresAction: true,
        clientSecret: paymentIntent.client\_secret
      };
    }
    
    // Payment successful
    return { success: true, paymentIntent };
    
  } catch (error) {
    if (error.code === 'authentication\_required') {
      // The payment requires authentication
      const paymentIntentRetrieved = await stripe.paymentIntents.retrieve(error.raw.payment\_intent.id);
      return {
        requiresAction: true,
        clientSecret: paymentIntentRetrieved.client\_secret
      };
    }
    
    console.error('Error charging customer:', error);
    return { error: error.message };
  }
}

 

Step 6: Alternative Method - Using the Charges API (Legacy)

 

While PaymentIntents is recommended, you can also use the legacy Charges API:

Node.js:


async function chargeCustomerLegacy(customerId, cardId, amount) {
  try {
    const charge = await stripe.charges.create({
      amount: amount,
      currency: 'usd',
      customer: customerId,
      source: cardId, // This is the card ID, not the payment method ID
    });
    
    console.log(`Charge successful: ${charge.id}`);
    return charge;
  } catch (error) {
    console.error('Error charging customer:', error);
    throw error;
  }
}

// Example usage
chargeCustomerLegacy('cus_AbCdEfGhIjKlMn', 'card_1234567890', 2000);

Python:


def charge_customer_legacy(customer_id, card_id, amount):
    try:
        charge = stripe.Charge.create(
            amount=amount,
            currency='usd',
            customer=customer\_id,
            source=card\_id,  # This is the card ID, not the payment method ID
        )
        
        print(f"Charge successful: {charge.id}")
        return charge
    except Exception as e:
        print(f"Error charging customer: {e}")
        raise e

# Example usage
charge_customer_legacy('cus_AbCdEfGhIjKlMn', 'card_1234567890', 2000)

 

Step 7: Implement Error Handling and Recovery

 

When charging saved cards, you might encounter various errors. Here's a more robust implementation with error handling:

Node.js:


async function chargeCustomerWithErrorHandling(customerId, paymentMethodId, amount) {
  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: 'usd',
      customer: customerId,
      payment\_method: paymentMethodId,
      off\_session: true,
      confirm: true,
    });
    
    return { success: true, paymentIntent };
    
  } catch (error) {
    // Different error types require different handling
    if (error.type === 'StripeCardError') {
      // Card declined
      console.error('Card was declined:', error.message);
      return { error: 'Your card was declined.', details: error.message };
    } else if (error.code === 'authentication\_required') {
      // 3D Secure authentication needed
      const paymentIntentRetrieved = await stripe.paymentIntents.retrieve(error.raw.payment\_intent.id);
      return {
        requiresAction: true,
        clientSecret: paymentIntentRetrieved.client\_secret
      };
    } else if (error.code === 'card\_declined') {
      // Specific decline code
      return { error: 'Your card was declined.', declineCode: error.decline\_code };
    } else {
      // Other types of errors
      console.error('Other error occurred:', error.message);
      return { error: 'An error occurred while processing your payment.' };
    }
  }
}

 

Step 8: Set Up a Webhook to Monitor Payment Status

 

It's crucial to set up webhooks to monitor the status of payments, especially for asynchronous processes like 3D Secure authentication:

Node.js (Express):


const express = require('express');
const app = express();

// This is your Stripe CLI webhook secret for testing
const endpointSecret = 'whsec_your_webhook\_secret';

app.post('/webhook', express.raw({type: 'application/json'}), (request, response) => {
  const sig = request.headers['stripe-signature'];

  let event;

  try {
    event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
  } catch (err) {
    response.status(400).send(`Webhook Error: ${err.message}`);
    return;
  }

  // Handle the event
  switch (event.type) {
    case 'payment\_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log(`PaymentIntent ${paymentIntent.id} succeeded`);
      // Then define and call a function to handle the successful payment
      handleSuccessfulPayment(paymentIntent);
      break;
    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      console.log(`PaymentIntent ${failedPaymentIntent.id} failed`);
      // Then define and call a function to handle the failed payment
      handleFailedPayment(failedPaymentIntent);
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  // Return a 200 response to acknowledge receipt of the event
  response.send();
});

app.listen(3000, () => console.log('Running on port 3000'));

function handleSuccessfulPayment(paymentIntent) {
  // Update your database
  // Fulfill the order
  // Notify the customer, etc.
}

function handleFailedPayment(paymentIntent) {
  // Update your database
  // Notify the customer
  // Maybe retry with a different payment method, etc.
}

 

Step 9: Implement a Client-Side Solution for Authentication Requirements

 

If a payment requires additional authentication, you'll need a client-side solution to handle it. Here's a basic implementation using JavaScript and Stripe.js:

HTML:





  Handle Authentication
  


  

 

Step 10: Testing Your Implementation

 

Before going live, thoroughly test your implementation:

  • Use Stripe's test cards (e.g., 4242 4242 4242 4242 for successful payments)
  • Test with a card that requires authentication (e.g., 4000 0027 6000 3184)
  • Test with a card that will be declined (e.g., 4000 0000 0000 0002)
  • Test handling of expired cards, insufficient funds, etc.

 

Step 11: Going Live

 

When you're ready to go live:

  • Switch from test API keys to live API keys
  • Update your webhook endpoints to use live webhook signing secrets
  • Monitor your Stripe Dashboard for transactions and issues
  • Set up monitoring and alerts for payment failures

 

Conclusion

 

Charging saved cards with Stripe provides a seamless experience for returning customers while maintaining security and compliance with regulations like SCA. The PaymentIntents API is the recommended approach for most scenarios, but you can also use the Charges API for simpler cases.

Remember to handle errors gracefully, implement proper server-side validation, and set up webhooks to keep your system in sync with Stripe's payment status updates.

By following this tutorial, you've learned how to charge saved cards securely, handle authentication requirements, and manage potential errors - all essential components of a robust payment system.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022