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

How to test 3D Secure payments in Stripe

Test 3D Secure (3DS) payments in Stripe using test card 4000000000003220 (triggers 3DS that can succeed or fail) and 4000008400001629 (always fails 3DS). Your PaymentIntent will return status requires_action when 3DS is needed, and your frontend must redirect the customer to complete authentication before the payment can succeed.

What you'll learn

  • How 3D Secure authentication works in the Stripe payment flow
  • Which test cards trigger 3DS and how to use them
  • How to handle the requires_action PaymentIntent status on your frontend
  • How to test both successful and failed 3DS authentication
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate5 min read20 minutesStripe API v2024-12+, Node.js 18+, Stripe.js on frontendMarch 2026RapidDev Engineering Team
TL;DR

Test 3D Secure (3DS) payments in Stripe using test card 4000000000003220 (triggers 3DS that can succeed or fail) and 4000008400001629 (always fails 3DS). Your PaymentIntent will return status requires_action when 3DS is needed, and your frontend must redirect the customer to complete authentication before the payment can succeed.

What Is 3D Secure and Why Test It?

3D Secure (3DS) is an authentication protocol that adds an extra verification step during online card payments. The customer is redirected to their bank to confirm the payment with a password, SMS code, or biometric. Under European SCA (Strong Customer Authentication) regulations, 3DS is required for most card payments. Stripe's Radar rules may also trigger 3DS for high-risk transactions. Testing 3DS ensures your checkout flow handles the authentication redirect, success callback, and failure fallback correctly.

Prerequisites

  • A Stripe account in test mode
  • Node.js 18 or newer installed
  • The stripe npm package on the server and @stripe/stripe-js on the frontend
  • Your test API keys (pk_test_ and sk_test_) from Dashboard → Developers → API keys

Step-by-step guide

1

Create a PaymentIntent on the server

Create a PaymentIntent with confirm: true and a 3DS test card. The response will have status requires_action when 3DS is triggered.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3const paymentIntent = await stripe.paymentIntents.create({
4 amount: 5000, // $50.00
5 currency: 'usd',
6 payment_method: 'pm_card_threeDSecure2Required',
7 confirm: true,
8 return_url: 'https://yoursite.com/payment-result',
9});
10
11console.log('Status:', paymentIntent.status);
12// 'requires_action' — 3DS authentication needed

Expected result: PaymentIntent status is requires_action with a next_action object containing the 3DS redirect URL.

2

Handle 3DS on the frontend with Stripe.js

Use stripe.confirmCardPayment() on the frontend, which automatically handles the 3DS popup or redirect. Stripe.js manages the authentication flow and returns the result.

typescript
1// Frontend JavaScript
2const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');
3
4async function handlePayment(clientSecret) {
5 const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret);
6
7 if (error) {
8 // 3DS failed or card declined after 3DS
9 console.error('Payment failed:', error.message);
10 document.getElementById('error').textContent = error.message;
11 } else if (paymentIntent.status === 'succeeded') {
12 console.log('Payment succeeded!');
13 window.location.href = '/success';
14 }
15}

Expected result: A 3DS authentication popup appears. Clicking 'Complete' succeeds; clicking 'Fail' returns an error.

3

Test with cards that always require 3DS

Use these test cards in your Stripe Elements form to trigger different 3DS scenarios.

typescript
1// Always requires 3DS, authentication can succeed or fail:
2// Card: 4000000000003220
3// Token: pm_card_threeDSecure2Required
4
5// Requires 3DS, authentication always fails:
6// Card: 4000008400001629
7
8// Requires 3DS, authentication succeeds:
9// Card: 4000000000003055
10
11// 3DS is supported but not required (Radar may trigger it):
12// Card: 4000000000003063
13
14// Use these with any future expiry, any 3-digit CVC, any postal code

Expected result: Each card triggers the appropriate 3DS behavior in Stripe's test environment.

4

Verify the result via webhook

Set up a webhook to listen for payment_intent.succeeded and payment_intent.payment_failed events. This is the most reliable way to confirm the outcome after 3DS.

typescript
1const express = require('express');
2const app = express();
3
4app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
5 const sig = req.headers['stripe-signature'];
6 let event;
7
8 try {
9 event = stripe.webhooks.constructEvent(
10 req.body,
11 sig,
12 process.env.STRIPE_WEBHOOK_SECRET
13 );
14 } catch (err) {
15 return res.status(400).send(`Webhook Error: ${err.message}`);
16 }
17
18 if (event.type === 'payment_intent.succeeded') {
19 console.log('Payment confirmed after 3DS:', event.data.object.id);
20 } else if (event.type === 'payment_intent.payment_failed') {
21 console.log('Payment failed after 3DS:', event.data.object.id);
22 }
23
24 res.json({ received: true });
25});

Expected result: Webhook receives the succeeded or payment_failed event after the customer completes or fails 3DS authentication.

Complete working example

three-d-secure.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5app.use(express.static('public'));
6
7// Create PaymentIntent for 3DS testing
8app.post('/api/create-payment', express.json(), async (req, res) => {
9 try {
10 const { amount } = req.body;
11
12 const paymentIntent = await stripe.paymentIntents.create({
13 amount: amount || 5000,
14 currency: 'usd',
15 automatic_payment_methods: { enabled: true },
16 });
17
18 res.json({ clientSecret: paymentIntent.client_secret });
19 } catch (err) {
20 res.status(500).json({ error: err.message });
21 }
22});
23
24// Webhook to confirm 3DS result
25app.post('/webhook',
26 express.raw({ type: 'application/json' }),
27 (req, res) => {
28 const sig = req.headers['stripe-signature'];
29 let event;
30
31 try {
32 event = stripe.webhooks.constructEvent(
33 req.body,
34 sig,
35 process.env.STRIPE_WEBHOOK_SECRET
36 );
37 } catch (err) {
38 console.error('Webhook signature verification failed:', err.message);
39 return res.status(400).send(`Webhook Error: ${err.message}`);
40 }
41
42 switch (event.type) {
43 case 'payment_intent.succeeded':
44 console.log('Payment succeeded (post-3DS):', event.data.object.id);
45 break;
46 case 'payment_intent.payment_failed':
47 console.log('Payment failed (post-3DS):', event.data.object.id);
48 break;
49 case 'payment_intent.requires_action':
50 console.log('Awaiting 3DS authentication:', event.data.object.id);
51 break;
52 }
53
54 res.json({ received: true });
55 }
56);
57
58const PORT = process.env.PORT || 3000;
59app.listen(PORT, () => console.log(`Server on port ${PORT}`));

Common mistakes when testing 3D Secure payments in Stripe

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

How to avoid: When a PaymentIntent returns requires_action, you must call stripe.confirmCardPayment() with the client_secret to trigger the 3DS popup. Ignoring this status means the payment never completes.

Why it's a problem: Using stripe.confirmCardPayment without the payment method

How to avoid: If the PaymentIntent already has a payment method attached and confirmed, just pass the client_secret. If not, include the payment_method option with the Element or pm_ ID.

Why it's a problem: Not testing the 3DS failure path

How to avoid: Use card 4000008400001629 to test failed 3DS authentication. Your app must handle this gracefully and let the customer try again.

Why it's a problem: Relying only on the frontend result instead of webhooks

How to avoid: The customer might close the browser after 3DS. Use payment_intent.succeeded webhook as the source of truth for payment confirmation.

Best practices

  • Always use stripe.confirmCardPayment() on the frontend to handle 3DS automatically
  • Test all three scenarios: 3DS success (4000000000003055), 3DS user-choice (4000000000003220), and 3DS failure (4000008400001629)
  • Set up webhooks for payment_intent.succeeded and payment_intent.payment_failed as the source of truth
  • Use express.raw() for the webhook endpoint to preserve the raw body needed for signature verification
  • Show a loading state while the 3DS authentication popup is open
  • Provide a clear error message and retry option when 3DS fails
  • Test on mobile devices — 3DS popups may behave differently on mobile browsers

Still stuck?

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

ChatGPT Prompt

Write a full-stack Node.js/Express + frontend JavaScript flow for Stripe 3D Secure payments. The server creates a PaymentIntent, the frontend uses stripe.confirmCardPayment() to handle 3DS, and a webhook endpoint confirms the result using stripe.webhooks.constructEvent with raw body.

Stripe Prompt

Add 3D Secure support to my Stripe payment flow. Handle the requires_action status on the frontend with stripe.confirmCardPayment(), show appropriate loading and error states, and set up a webhook to confirm the payment outcome after authentication.

Frequently asked questions

What does the 3DS test popup look like?

In Stripe's test mode, the 3DS popup shows a simple page with 'Complete authentication' and 'Fail authentication' buttons. In production, customers see their bank's actual authentication page.

Can I skip 3DS for returning customers?

Stripe Radar can request exemptions for low-risk transactions or returning customers. However, the issuing bank has final authority on whether to enforce 3DS.

Does 3DS work with Stripe Checkout?

Yes. Stripe Checkout handles 3DS automatically. No additional code is needed — Stripe manages the authentication flow within the hosted checkout page.

What happens if the customer closes the 3DS popup?

The PaymentIntent stays in requires_action status. The customer can return and retry. The PaymentIntent expires after 24 hours if not completed.

Is 3DS required for all countries?

3DS is required in the European Economic Area (EEA) and UK under SCA regulations. Other regions may not require it, but issuers can still request it.

What if I need help implementing 3D Secure across a complex payment system?

For multi-currency, multi-region payment systems with varying 3DS requirements and exemption strategies, the RapidDev team can help architect and implement a compliant solution.

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.