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

How to handle SCA (Strong Customer Authentication) in Stripe

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.

What you'll learn

  • What SCA/PSD2 is and when it applies to your payments
  • How Stripe's PaymentIntents API handles SCA automatically
  • How to handle the requires_action status for 3D Secure challenges
  • How SCA exemptions work for low-risk and recurring payments
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced6 min read25 minutesStripe API v2024-12+, Node.js 18+, Stripe.js v3+March 2026RapidDev Engineering Team
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.

Prerequisites

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

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3async function createPayment(amount, paymentMethodId) {
4 const paymentIntent = await stripe.paymentIntents.create({
5 amount: amount, // in cents
6 currency: 'eur',
7 payment_method: paymentMethodId,
8 confirmation_method: 'manual',
9 confirm: true,
10 payment_method_options: {
11 card: {
12 request_three_d_secure: 'automatic', // Let Stripe decide
13 },
14 },
15 return_url: 'https://yoursite.com/payment/complete',
16 });
17
18 return paymentIntent;
19}

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.

typescript
1const express = require('express');
2const app = express();
3app.use(express.json());
4
5app.post('/api/pay', async (req, res) => {
6 try {
7 const { paymentMethodId, amount } = req.body;
8
9 const paymentIntent = await stripe.paymentIntents.create({
10 amount,
11 currency: 'eur',
12 payment_method: paymentMethodId,
13 confirm: true,
14 automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
15 });
16
17 if (paymentIntent.status === 'requires_action') {
18 // Customer needs to authenticate
19 res.json({
20 requiresAction: true,
21 clientSecret: paymentIntent.client_secret,
22 });
23 } else if (paymentIntent.status === 'succeeded') {
24 res.json({ success: true });
25 } else {
26 res.json({ status: paymentIntent.status });
27 }
28 } catch (err) {
29 res.status(400).json({ error: err.message });
30 }
31});

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.

typescript
1// Frontend JavaScript
2const stripe = Stripe('pk_test_yourPublishableKey');
3
4async function handlePayment(paymentMethodId, amount) {
5 const response = await fetch('/api/pay', {
6 method: 'POST',
7 headers: { 'Content-Type': 'application/json' },
8 body: JSON.stringify({ paymentMethodId, amount }),
9 });
10 const data = await response.json();
11
12 if (data.requiresAction) {
13 // Show the 3D Secure challenge
14 const { paymentIntent, error } = await stripe.handleCardAction(
15 data.clientSecret
16 );
17
18 if (error) {
19 showError(error.message);
20 return;
21 }
22
23 // Confirm the payment on the server after authentication
24 const confirmResponse = await fetch('/api/confirm-payment', {
25 method: 'POST',
26 headers: { 'Content-Type': 'application/json' },
27 body: JSON.stringify({ paymentIntentId: paymentIntent.id }),
28 });
29 const confirmData = await confirmResponse.json();
30 showResult(confirmData);
31 } else if (data.success) {
32 showSuccess('Payment completed!');
33 }
34}

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.

typescript
1app.post('/api/confirm-payment', async (req, res) => {
2 try {
3 const paymentIntent = await stripe.paymentIntents.confirm(
4 req.body.paymentIntentId
5 );
6
7 if (paymentIntent.status === 'succeeded') {
8 res.json({ success: true });
9 } else if (paymentIntent.status === 'requires_action') {
10 // Customer failed authentication
11 res.json({ requiresAction: true, clientSecret: paymentIntent.client_secret });
12 } else {
13 res.json({ status: paymentIntent.status });
14 }
15 } catch (err) {
16 res.status(400).json({ error: err.message });
17 }
18});

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.

typescript
1// SCA test cards (use any future expiry and any 3-digit CVC):
2//
3// 4000002500003155 — Requires authentication (always triggers 3DS)
4// 4000002760003184 — Requires authentication (always triggers 3DS)
5// 4000008260003178 — Authentication fails
6// 4242424242424242 — No authentication required (non-SCA card)
7// 4000003800000446 — SCA required, successful authentication
8
9// Example: Create a payment with a card that requires SCA
10async function testSCA() {
11 const pm = await stripe.paymentMethods.create({
12 type: 'card',
13 card: {
14 number: '4000002500003155', // Requires authentication
15 exp_month: 12,
16 exp_year: 2027,
17 cvc: '123',
18 },
19 });
20
21 const pi = await stripe.paymentIntents.create({
22 amount: 2000,
23 currency: 'eur',
24 payment_method: pm.id,
25 confirm: true,
26 return_url: 'https://yoursite.com/complete',
27 });
28
29 console.log('Status:', pi.status); // 'requires_action'
30}

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

Complete working example

sca-payment-flow.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
8app.post('/api/create-payment', async (req, res) => {
9 try {
10 const { paymentMethodId, amount } = req.body;
11 const pi = await stripe.paymentIntents.create({
12 amount,
13 currency: 'eur',
14 payment_method: paymentMethodId,
15 confirm: true,
16 automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
17 });
18
19 res.json(handlePaymentStatus(pi));
20 } catch (err) {
21 if (err.type === 'StripeCardError') {
22 res.status(402).json({ error: err.message });
23 } else {
24 res.status(500).json({ error: 'Payment processing error' });
25 }
26 }
27});
28
29app.post('/api/confirm-payment', async (req, res) => {
30 try {
31 const pi = await stripe.paymentIntents.confirm(req.body.paymentIntentId);
32 res.json(handlePaymentStatus(pi));
33 } catch (err) {
34 res.status(400).json({ error: err.message });
35 }
36});
37
38function handlePaymentStatus(pi) {
39 switch (pi.status) {
40 case 'succeeded':
41 return { success: true, paymentIntentId: pi.id };
42 case 'requires_action':
43 return { requiresAction: true, clientSecret: pi.client_secret };
44 case 'requires_payment_method':
45 return { error: 'Payment method failed. Please try another card.' };
46 default:
47 return { status: pi.status };
48 }
49}
50
51app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
52 const sig = req.headers['stripe-signature'];
53 let event;
54 try {
55 event = stripe.webhooks.constructEvent(
56 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
57 );
58 } catch (err) {
59 return res.status(400).send(`Webhook Error: ${err.message}`);
60 }
61 if (event.type === 'payment_intent.succeeded') {
62 console.log('Payment confirmed:', event.data.object.id);
63 } else if (event.type === 'payment_intent.payment_failed') {
64 console.log('Payment failed:', event.data.object.id);
65 }
66 res.json({ received: true });
67});
68
69app.listen(3000, () => console.log('SCA payment server on port 3000'));

Common mistakes when handling SCA (Strong Customer Authentication) in Stripe

Why it's a problem: Not handling the requires_action PaymentIntent status

How to avoid: Always check for requires_action and present the 3D Secure challenge. Ignoring it means the payment never completes.

Why it's a problem: Testing only with 4242424242424242 which does not trigger SCA

How to avoid: Use SCA-specific test cards like 4000002500003155 to test the full authentication flow.

Why it's a problem: Using Charges API instead of PaymentIntents for European payments

How to avoid: The Charges API does not support SCA. Always use PaymentIntents for SCA-compliant payments.

Why it's a problem: Redirecting without setting return_url

How to avoid: 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

Still stuck?

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

ChatGPT Prompt

How do I handle SCA (Strong Customer Authentication) in Stripe? My customers are in Europe and some payments require 3D Secure. Show me the full PaymentIntent flow with requires_action handling in Node.js.

Stripe Prompt

Build an SCA-compliant payment flow for my European e-commerce site. I need a server endpoint that creates PaymentIntents, handles requires_action for 3D Secure, and confirms payments after authentication. Use Node.js and Express.

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.

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.