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
Create a PaymentIntent with automatic SCA handling
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.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23async function createPayment(amount, paymentMethodId) {4 const paymentIntent = await stripe.paymentIntents.create({5 amount: amount, // in cents6 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 decide13 },14 },15 return_url: 'https://yoursite.com/payment/complete',16 });1718 return paymentIntent;19}Expected result: A PaymentIntent is created. If SCA is required, its status will be requires_action.
Handle the requires_action status on the server
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.
1const express = require('express');2const app = express();3app.use(express.json());45app.post('/api/pay', async (req, res) => {6 try {7 const { paymentMethodId, amount } = req.body;89 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 });1617 if (paymentIntent.status === 'requires_action') {18 // Customer needs to authenticate19 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.
Complete authentication on the frontend
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.
1// Frontend JavaScript2const stripe = Stripe('pk_test_yourPublishableKey');34async 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();1112 if (data.requiresAction) {13 // Show the 3D Secure challenge14 const { paymentIntent, error } = await stripe.handleCardAction(15 data.clientSecret16 );1718 if (error) {19 showError(error.message);20 return;21 }2223 // Confirm the payment on the server after authentication24 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.
Confirm the payment after authentication
Confirm the payment after authentication
After the customer completes 3D Secure, confirm the PaymentIntent on the server to finalize the payment.
1app.post('/api/confirm-payment', async (req, res) => {2 try {3 const paymentIntent = await stripe.paymentIntents.confirm(4 req.body.paymentIntentId5 );67 if (paymentIntent.status === 'succeeded') {8 res.json({ success: true });9 } else if (paymentIntent.status === 'requires_action') {10 // Customer failed authentication11 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.
Test with SCA test cards
Test with SCA test cards
Stripe provides test cards that simulate different SCA scenarios.
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 fails6// 4242424242424242 — No authentication required (non-SCA card)7// 4000003800000446 — SCA required, successful authentication89// Example: Create a payment with a card that requires SCA10async function testSCA() {11 const pm = await stripe.paymentMethods.create({12 type: 'card',13 card: {14 number: '4000002500003155', // Requires authentication15 exp_month: 12,16 exp_year: 2027,17 cvc: '123',18 },19 });2021 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 });2829 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
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);2const express = require('express');3const app = express();45app.use(express.json());6app.use(express.static('public'));78app.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 });1819 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});2829app.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});3738function 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}5051app.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_SECRET57 );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});6869app.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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation