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

How to store card details securely with Stripe

Never store raw card numbers on your server. Stripe handles PCI compliance through tokenization — card details are collected client-side with Stripe Elements or Checkout, converted to tokens or PaymentMethods, and only the token ID touches your backend. This guide covers SetupIntents for saving cards, PaymentMethods for reuse, and how Stripe's tokenization keeps you PCI-compliant.

What you'll learn

  • How Stripe tokenization works and why you never store raw card numbers
  • How to use SetupIntents to save cards for future payments
  • How to attach PaymentMethods to customers for reuse
  • What PCI compliance level Stripe Elements provides
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read15 minutesStripe API v2024-12+, Node.js 18+, Stripe.js v3+March 2026RapidDev Engineering Team
TL;DR

Never store raw card numbers on your server. Stripe handles PCI compliance through tokenization — card details are collected client-side with Stripe Elements or Checkout, converted to tokens or PaymentMethods, and only the token ID touches your backend. This guide covers SetupIntents for saving cards, PaymentMethods for reuse, and how Stripe's tokenization keeps you PCI-compliant.

Secure Card Storage with Stripe Tokenization

PCI DSS (Payment Card Industry Data Security Standard) requires any business handling card data to follow strict security rules. Stripe eliminates most of this burden through tokenization. Card numbers never touch your server — they go directly from the customer's browser to Stripe's PCI-compliant infrastructure. Your server only ever handles opaque token IDs like pm_1ABC or tok_1XYZ.

Prerequisites

  • A Stripe account (test mode is fine)
  • Node.js 18 or later installed
  • Stripe Node.js SDK: npm install stripe
  • A basic frontend with Stripe.js loaded

Step-by-step guide

1

Understand the tokenization flow

Card details flow from the customer's browser directly to Stripe via Stripe.js. Stripe returns a token or PaymentMethod ID that your server uses. Your server never sees the full card number.

typescript
1// Frontend: Load Stripe.js and create Elements
2// <script src="https://js.stripe.com/v3/"></script>
3
4// Initialize with your PUBLISHABLE key (pk_test_...)
5const stripe = Stripe('pk_test_yourPublishableKey');
6const elements = stripe.elements();
7const cardElement = elements.create('card');
8cardElement.mount('#card-element');
9
10// When the form is submitted, create a PaymentMethod
11async function handleSubmit() {
12 const { paymentMethod, error } = await stripe.createPaymentMethod({
13 type: 'card',
14 card: cardElement,
15 });
16
17 if (error) {
18 console.error(error.message);
19 return;
20 }
21
22 // Send ONLY the paymentMethod.id to your server
23 await fetch('/api/save-card', {
24 method: 'POST',
25 headers: { 'Content-Type': 'application/json' },
26 body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
27 });
28}

Expected result: A PaymentMethod ID like pm_1ABC is created client-side and sent to your server. No card numbers touch your backend.

2

Create a SetupIntent to save a card for later

SetupIntents handle the flow of saving a card without charging it. They also handle 3D Secure authentication if required by the card issuer.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3// Server: Create a SetupIntent
4async function createSetupIntent(customerId) {
5 const setupIntent = await stripe.setupIntents.create({
6 customer: customerId,
7 payment_method_types: ['card'],
8 });
9 return setupIntent.client_secret; // Send to frontend
10}
11
12// Frontend: Confirm the SetupIntent with the card element
13async function confirmSetup(clientSecret) {
14 const { setupIntent, error } = await stripe.confirmCardSetup(clientSecret, {
15 payment_method: {
16 card: cardElement,
17 },
18 });
19
20 if (error) {
21 console.error(error.message);
22 } else {
23 console.log('Card saved:', setupIntent.payment_method);
24 }
25}

Expected result: The card is tokenized, authenticated if needed, and attached to the customer for future charges.

3

Attach a PaymentMethod to a customer

After creating a PaymentMethod on the frontend, attach it to a Stripe Customer on the backend so you can charge it later.

typescript
1async function saveCardToCustomer(paymentMethodId, customerId) {
2 // Attach the PaymentMethod to the customer
3 await stripe.paymentMethods.attach(paymentMethodId, {
4 customer: customerId,
5 });
6
7 // Optionally set as default payment method
8 await stripe.customers.update(customerId, {
9 invoice_settings: {
10 default_payment_method: paymentMethodId,
11 },
12 });
13
14 console.log(`Card ${paymentMethodId} saved to customer ${customerId}`);
15}
16
17saveCardToCustomer('pm_1ABC', 'cus_customer123');

Expected result: The PaymentMethod is attached to the customer and set as the default for future invoices and charges.

4

Charge a saved card

Create a PaymentIntent with the customer and their saved PaymentMethod to charge the card without collecting details again.

typescript
1async function chargeSavedCard(customerId, paymentMethodId, amount) {
2 const paymentIntent = await stripe.paymentIntents.create({
3 amount: amount, // in cents
4 currency: 'usd',
5 customer: customerId,
6 payment_method: paymentMethodId,
7 off_session: true, // Customer is not present
8 confirm: true,
9 });
10
11 console.log('Payment succeeded:', paymentIntent.id);
12 return paymentIntent;
13}
14
15chargeSavedCard('cus_customer123', 'pm_1ABC', 2500); // $25.00

Expected result: The saved card is charged without requiring the customer to re-enter card details.

Complete working example

secure-card-storage.js
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2const express = require('express');
3const app = express();
4
5app.use(express.json());
6app.use(express.static('public'));
7
8// Create a customer and SetupIntent for saving a card
9app.post('/api/setup-card', async (req, res) => {
10 try {
11 const { email } = req.body;
12
13 // Create or retrieve customer
14 let customer;
15 const existing = await stripe.customers.list({ email, limit: 1 });
16 if (existing.data.length > 0) {
17 customer = existing.data[0];
18 } else {
19 customer = await stripe.customers.create({ email });
20 }
21
22 // Create SetupIntent
23 const setupIntent = await stripe.setupIntents.create({
24 customer: customer.id,
25 payment_method_types: ['card'],
26 });
27
28 res.json({
29 clientSecret: setupIntent.client_secret,
30 customerId: customer.id,
31 });
32 } catch (err) {
33 res.status(400).json({ error: err.message });
34 }
35});
36
37// List saved cards for a customer
38app.get('/api/cards/:customerId', async (req, res) => {
39 try {
40 const methods = await stripe.paymentMethods.list({
41 customer: req.params.customerId,
42 type: 'card',
43 });
44
45 const cards = 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 res.json(cards);
54 } catch (err) {
55 res.status(400).json({ error: err.message });
56 }
57});
58
59// Charge a saved card
60app.post('/api/charge-saved', async (req, res) => {
61 try {
62 const { customerId, paymentMethodId, amount } = req.body;
63 const pi = await stripe.paymentIntents.create({
64 amount,
65 currency: 'usd',
66 customer: customerId,
67 payment_method: paymentMethodId,
68 off_session: true,
69 confirm: true,
70 });
71 res.json({ paymentIntentId: pi.id, status: pi.status });
72 } catch (err) {
73 if (err.code === 'authentication_required') {
74 res.status(402).json({
75 error: 'Authentication required',
76 paymentIntentId: err.raw.payment_intent.id,
77 });
78 } else {
79 res.status(400).json({ error: err.message });
80 }
81 }
82});
83
84app.listen(3000, () => console.log('Server on port 3000'));

Common mistakes when storing card details securely with Stripe

Why it's a problem: Logging or storing full card numbers in your database

How to avoid: Never store raw card data. Use Stripe's PaymentMethod IDs instead. You can store last4 and brand for display purposes.

Why it's a problem: Using the secret key (sk_) on the frontend

How to avoid: Only use the publishable key (pk_test_ or pk_live_) in browser code. Secret keys must stay on your server.

Why it's a problem: Not handling authentication_required errors on off-session charges

How to avoid: Some saved cards require 3D Secure authentication. Catch the error and redirect the customer to complete authentication.

Why it's a problem: Creating PaymentMethods server-side with raw card data

How to avoid: Always create PaymentMethods client-side with Stripe.js or Elements. Server-side creation with raw card data increases your PCI scope.

Best practices

  • Always collect card details with Stripe Elements or Checkout to stay PCI-compliant at SAQ-A level
  • Use SetupIntents (not raw tokens) when saving cards — they handle 3D Secure authentication
  • Store only the PaymentMethod ID, card brand, and last 4 digits in your database
  • Set a default_payment_method on the customer for seamless recurring charges
  • Handle authentication_required errors gracefully for off-session charges
  • Use test card 4242424242424242 with any future expiry and any CVC for testing
  • Delete saved PaymentMethods when customers request removal of their card data

Still stuck?

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

ChatGPT Prompt

How do I securely store credit card details with Stripe? I want to save a card for future charges without storing raw card numbers. Show me the SetupIntent and PaymentMethod flow with Node.js.

Stripe Prompt

Build a card-saving flow for my app using Stripe. I need a SetupIntent to save cards, a way to list saved cards, and an endpoint to charge saved cards off-session. Use Node.js and Express with Stripe Elements on the frontend.

Frequently asked questions

What PCI compliance level do I need when using Stripe Elements?

SAQ-A, the simplest level. Card data goes directly from the browser to Stripe and never touches your server. You just need to fill out a self-assessment questionnaire annually.

Can I store the card number in my database if I encrypt it?

No. Even encrypted card storage puts you at PCI SAQ-D level, requiring extensive security audits. Use Stripe's PaymentMethod IDs instead.

How do I let customers update their saved card?

Create a new SetupIntent, let the customer enter the new card, and then detach the old PaymentMethod. You cannot update card details on an existing PaymentMethod.

What is the difference between a Token and a PaymentMethod?

Tokens (tok_) are the older API. PaymentMethods (pm_) are the current standard and support SetupIntents, 3D Secure, and more card types. Always use PaymentMethods for new integrations.

How long does a saved PaymentMethod last?

PaymentMethods attached to a customer persist until the card expires, is declined, or you explicitly detach it. Stripe automatically updates card details for some banks via card account updater.

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.