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.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
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
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:
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:
Step 11: Going Live
When you're ready to go live:
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.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.