# How to handle SCA (Strong Customer Authentication) in Stripe

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

## TL;DR

Strong Customer Authentication (SCA) is a European regulation (PSD2) requiring two-factor authentication for online payments. Stripe handles SCA automatically through PaymentIntents — when a payment requires authentication, Stripe triggers a 3D Secure challenge. This guide covers how to build SCA-compliant payment flows, handle requires_action status, and manage exemptions for low-risk payments.

## Building SCA-Compliant Payment Flows with Stripe

Strong Customer Authentication (SCA) is part of the EU's PSD2 regulation. It requires European cardholders to verify their identity using at least two of three factors: something they know (password/PIN), something they have (phone/card), or something they are (biometric). Stripe handles SCA through its PaymentIntents API by automatically triggering 3D Secure when required. Your job is to handle the requires_action response correctly.

## Before you start

- Stripe account (SCA applies to European payments)
- Node.js 18 or later installed
- Stripe Node.js SDK: npm install stripe
- Stripe.js loaded on your frontend
- Understanding of PaymentIntents

## Step-by-step guide

### 1. Create a PaymentIntent with automatic SCA handling

Stripe's PaymentIntents API automatically requests SCA when needed. Set payment_method_options to enable automatic 3D Secure.

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

async function createPayment(amount, paymentMethodId) {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: amount, // in cents
    currency: 'eur',
    payment_method: paymentMethodId,
    confirmation_method: 'manual',
    confirm: true,
    payment_method_options: {
      card: {
        request_three_d_secure: 'automatic', // Let Stripe decide
      },
    },
    return_url: 'https://yoursite.com/payment/complete',
  });

  return paymentIntent;
}
```

**Expected result:** A PaymentIntent is created. If SCA is required, its status will be requires_action.

### 2. Handle the requires_action status on the server

When a PaymentIntent returns status requires_action, send the client_secret to the frontend so the customer can complete authentication.

```
const express = require('express');
const app = express();
app.use(express.json());

app.post('/api/pay', async (req, res) => {
  try {
    const { paymentMethodId, amount } = req.body;

    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency: 'eur',
      payment_method: paymentMethodId,
      confirm: true,
      automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
    });

    if (paymentIntent.status === 'requires_action') {
      // Customer needs to authenticate
      res.json({
        requiresAction: true,
        clientSecret: paymentIntent.client_secret,
      });
    } else if (paymentIntent.status === 'succeeded') {
      res.json({ success: true });
    } else {
      res.json({ status: paymentIntent.status });
    }
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});
```

**Expected result:** The server returns either a success response or a client_secret for the frontend to complete authentication.

### 3. Complete authentication on the frontend

When the server returns requiresAction: true, use stripe.handleCardAction() on the frontend to present the 3D Secure challenge to the customer.

```
// Frontend JavaScript
const stripe = Stripe('pk_test_yourPublishableKey');

async function handlePayment(paymentMethodId, amount) {
  const response = await fetch('/api/pay', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paymentMethodId, amount }),
  });
  const data = await response.json();

  if (data.requiresAction) {
    // Show the 3D Secure challenge
    const { paymentIntent, error } = await stripe.handleCardAction(
      data.clientSecret
    );

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

    // Confirm the payment on the server after authentication
    const confirmResponse = await fetch('/api/confirm-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ paymentIntentId: paymentIntent.id }),
    });
    const confirmData = await confirmResponse.json();
    showResult(confirmData);
  } else if (data.success) {
    showSuccess('Payment completed!');
  }
}
```

**Expected result:** The 3D Secure authentication modal appears. After the customer authenticates, the payment is confirmed.

### 4. Confirm the payment after authentication

After the customer completes 3D Secure, confirm the PaymentIntent on the server to finalize the payment.

```
app.post('/api/confirm-payment', async (req, res) => {
  try {
    const paymentIntent = await stripe.paymentIntents.confirm(
      req.body.paymentIntentId
    );

    if (paymentIntent.status === 'succeeded') {
      res.json({ success: true });
    } else if (paymentIntent.status === 'requires_action') {
      // Customer failed authentication
      res.json({ requiresAction: true, clientSecret: paymentIntent.client_secret });
    } else {
      res.json({ status: paymentIntent.status });
    }
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});
```

**Expected result:** The payment is confirmed and succeeds if the customer authenticated successfully.

### 5. Test with SCA test cards

Stripe provides test cards that simulate different SCA scenarios.

```
// SCA test cards (use any future expiry and any 3-digit CVC):
//
// 4000002500003155 — Requires authentication (always triggers 3DS)
// 4000002760003184 — Requires authentication (always triggers 3DS)
// 4000008260003178 — Authentication fails
// 4242424242424242 — No authentication required (non-SCA card)
// 4000003800000446 — SCA required, successful authentication

// Example: Create a payment with a card that requires SCA
async function testSCA() {
  const pm = await stripe.paymentMethods.create({
    type: 'card',
    card: {
      number: '4000002500003155', // Requires authentication
      exp_month: 12,
      exp_year: 2027,
      cvc: '123',
    },
  });

  const pi = await stripe.paymentIntents.create({
    amount: 2000,
    currency: 'eur',
    payment_method: pm.id,
    confirm: true,
    return_url: 'https://yoursite.com/complete',
  });

  console.log('Status:', pi.status); // 'requires_action'
}
```

**Expected result:** The PaymentIntent returns status requires_action, confirming your SCA flow works correctly.

## Complete code example

File: `sca-payment-flow.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'));

app.post('/api/create-payment', async (req, res) => {
  try {
    const { paymentMethodId, amount } = req.body;
    const pi = await stripe.paymentIntents.create({
      amount,
      currency: 'eur',
      payment_method: paymentMethodId,
      confirm: true,
      automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
    });

    res.json(handlePaymentStatus(pi));
  } catch (err) {
    if (err.type === 'StripeCardError') {
      res.status(402).json({ error: err.message });
    } else {
      res.status(500).json({ error: 'Payment processing error' });
    }
  }
});

app.post('/api/confirm-payment', async (req, res) => {
  try {
    const pi = await stripe.paymentIntents.confirm(req.body.paymentIntentId);
    res.json(handlePaymentStatus(pi));
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

function handlePaymentStatus(pi) {
  switch (pi.status) {
    case 'succeeded':
      return { success: true, paymentIntentId: pi.id };
    case 'requires_action':
      return { requiresAction: true, clientSecret: pi.client_secret };
    case 'requires_payment_method':
      return { error: 'Payment method failed. Please try another card.' };
    default:
      return { status: pi.status };
  }
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  if (event.type === 'payment_intent.succeeded') {
    console.log('Payment confirmed:', event.data.object.id);
  } else if (event.type === 'payment_intent.payment_failed') {
    console.log('Payment failed:', event.data.object.id);
  }
  res.json({ received: true });
});

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

## Common mistakes

- **Not handling the requires_action PaymentIntent status** — undefined Fix: Always check for requires_action and present the 3D Secure challenge. Ignoring it means the payment never completes.
- **Testing only with 4242424242424242 which does not trigger SCA** — undefined Fix: Use SCA-specific test cards like 4000002500003155 to test the full authentication flow.
- **Using Charges API instead of PaymentIntents for European payments** — undefined Fix: The Charges API does not support SCA. Always use PaymentIntents for SCA-compliant payments.
- **Redirecting without setting return_url** — undefined Fix: Some 3D Secure flows use redirects. Always set return_url on the PaymentIntent so the customer can return to your site.

## Best practices

- Use PaymentIntents with automatic 3D Secure — Stripe decides when authentication is needed
- Always handle requires_action status in your payment flow
- Test with SCA-required test cards (4000002500003155) before launching in Europe
- Use payment_intent.succeeded webhook as the authoritative confirmation, not the client response
- Set request_three_d_secure to 'automatic' to let Stripe optimize for conversion
- For recurring payments, use SetupIntents to authenticate the first payment, then charge off-session
- Handle payment_intent.payment_failed webhooks to notify customers of authentication failures

## Frequently asked questions

### Does SCA apply to all payments?

No. SCA primarily applies to online card payments where both the cardholder's bank and the merchant's bank are in the European Economic Area (EEA). Exemptions exist for low-value transactions, trusted beneficiaries, and recurring payments.

### What happens if I do not implement SCA?

Payments requiring SCA that are not authenticated will be declined by the cardholder's bank. Your conversion rate will drop significantly for European customers.

### Does Stripe handle SCA automatically?

Yes, when using PaymentIntents. Stripe automatically triggers 3D Secure when the issuing bank requires it. You just need to handle the requires_action response in your code.

### Are there exemptions to SCA?

Yes. Low-value transactions (under 30 EUR), merchant-initiated transactions (subscriptions after first payment), trusted beneficiaries, and low-risk transactions (based on Stripe's fraud analysis) can be exempted.

### How does SCA affect subscription payments?

The first payment requires full authentication. Subsequent recurring charges are merchant-initiated and can be exempted from SCA, as long as the initial SetupIntent was authenticated.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-handle-sca-strong-customer-authentication-in-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-handle-sca-strong-customer-authentication-in-stripe
