# How to store card details securely with Stripe

- Tool: Stripe
- Difficulty: Beginner
- Time required: 15 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+, Stripe.js v3+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
// Frontend: Load Stripe.js and create Elements
// <script src="https://js.stripe.com/v3/"></script>

// Initialize with your PUBLISHABLE key (pk_test_...)
const stripe = Stripe('pk_test_yourPublishableKey');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');

// When the form is submitted, create a PaymentMethod
async function handleSubmit() {
  const { paymentMethod, error } = await stripe.createPaymentMethod({
    type: 'card',
    card: cardElement,
  });

  if (error) {
    console.error(error.message);
    return;
  }

  // Send ONLY the paymentMethod.id to your server
  await fetch('/api/save-card', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paymentMethodId: paymentMethod.id }),
  });
}
```

**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.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Server: Create a SetupIntent
async function createSetupIntent(customerId) {
  const setupIntent = await stripe.setupIntents.create({
    customer: customerId,
    payment_method_types: ['card'],
  });
  return setupIntent.client_secret; // Send to frontend
}

// Frontend: Confirm the SetupIntent with the card element
async function confirmSetup(clientSecret) {
  const { setupIntent, error } = await stripe.confirmCardSetup(clientSecret, {
    payment_method: {
      card: cardElement,
    },
  });

  if (error) {
    console.error(error.message);
  } else {
    console.log('Card saved:', setupIntent.payment_method);
  }
}
```

**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.

```
async function saveCardToCustomer(paymentMethodId, customerId) {
  // Attach the PaymentMethod to the customer
  await stripe.paymentMethods.attach(paymentMethodId, {
    customer: customerId,
  });

  // Optionally set as default payment method
  await stripe.customers.update(customerId, {
    invoice_settings: {
      default_payment_method: paymentMethodId,
    },
  });

  console.log(`Card ${paymentMethodId} saved to customer ${customerId}`);
}

saveCardToCustomer('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.

```
async function chargeSavedCard(customerId, paymentMethodId, amount) {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: amount, // in cents
    currency: 'usd',
    customer: customerId,
    payment_method: paymentMethodId,
    off_session: true, // Customer is not present
    confirm: true,
  });

  console.log('Payment succeeded:', paymentIntent.id);
  return paymentIntent;
}

chargeSavedCard('cus_customer123', 'pm_1ABC', 2500); // $25.00
```

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

## Complete code example

File: `secure-card-storage.js`

```javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();

app.use(express.json());
app.use(express.static('public'));

// Create a customer and SetupIntent for saving a card
app.post('/api/setup-card', async (req, res) => {
  try {
    const { email } = req.body;

    // Create or retrieve customer
    let customer;
    const existing = await stripe.customers.list({ email, limit: 1 });
    if (existing.data.length > 0) {
      customer = existing.data[0];
    } else {
      customer = await stripe.customers.create({ email });
    }

    // Create SetupIntent
    const setupIntent = await stripe.setupIntents.create({
      customer: customer.id,
      payment_method_types: ['card'],
    });

    res.json({
      clientSecret: setupIntent.client_secret,
      customerId: customer.id,
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// List saved cards for a customer
app.get('/api/cards/:customerId', async (req, res) => {
  try {
    const methods = await stripe.paymentMethods.list({
      customer: req.params.customerId,
      type: 'card',
    });

    const cards = methods.data.map(pm => ({
      id: pm.id,
      brand: pm.card.brand,
      last4: pm.card.last4,
      expMonth: pm.card.exp_month,
      expYear: pm.card.exp_year,
    }));

    res.json(cards);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Charge a saved card
app.post('/api/charge-saved', async (req, res) => {
  try {
    const { customerId, paymentMethodId, amount } = req.body;
    const pi = await stripe.paymentIntents.create({
      amount,
      currency: 'usd',
      customer: customerId,
      payment_method: paymentMethodId,
      off_session: true,
      confirm: true,
    });
    res.json({ paymentIntentId: pi.id, status: pi.status });
  } catch (err) {
    if (err.code === 'authentication_required') {
      res.status(402).json({
        error: 'Authentication required',
        paymentIntentId: err.raw.payment_intent.id,
      });
    } else {
      res.status(400).json({ error: err.message });
    }
  }
});

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

## Common mistakes

- **Logging or storing full card numbers in your database** — undefined Fix: Never store raw card data. Use Stripe's PaymentMethod IDs instead. You can store last4 and brand for display purposes.
- **Using the secret key (sk_) on the frontend** — undefined Fix: Only use the publishable key (pk_test_ or pk_live_) in browser code. Secret keys must stay on your server.
- **Not handling authentication_required errors on off-session charges** — undefined Fix: Some saved cards require 3D Secure authentication. Catch the error and redirect the customer to complete authentication.
- **Creating PaymentMethods server-side with raw card data** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-store-card-details-securely-with-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-store-card-details-securely-with-stripe
