/stripe-guides

How to implement 3D Secure with Stripe Elements?

Learn how to implement 3D Secure with Stripe Elements step-by-step, including setup, payment form, server code, authentication, webhooks, and best practices.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to implement 3D Secure with Stripe Elements?

Implementing 3D Secure with Stripe Elements: A Comprehensive Tutorial

 

Step 1: Set Up Your Stripe Account

 

Before implementing 3D Secure with Stripe Elements, you need to have a Stripe account and API keys:

  • Sign up for a Stripe account at stripe.com if you don't already have one
  • Navigate to the Developers section in your Stripe Dashboard
  • Get your API keys (both publishable and secret keys)
  • For testing, use the test mode keys

 

Step 2: Include Stripe.js in Your HTML

 

Add the Stripe.js library to your HTML page:





  3D Secure Payment with Stripe
  
  


  


 

Step 3: Create a Payment Form with Stripe Elements

 

Add a payment form to your HTML:


 

Step 4: Initialize Stripe Elements

 

Add JavaScript to initialize Stripe and create the card element:


document.addEventListener('DOMContentLoaded', function() {
  // Initialize Stripe with your publishable key
  var stripe = Stripe('pk_test_your_publishable_key');
  
  // Create an instance of Elements
  var elements = stripe.elements();
  
  // Custom styling can be passed to options when creating an Element
  var style = {
    base: {
      color: '#32325d',
      fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
      fontSmoothing: 'antialiased',
      fontSize: '16px',
      '::placeholder': {
        color: '#aab7c4'
      }
    },
    invalid: {
      color: '#fa755a',
      iconColor: '#fa755a'
    }
  };
  
  // Create an instance of the card Element
  var card = elements.create('card', {style: style});
  
  // Add an instance of the card Element into the `card-element` div
  card.mount('#card-element');
  
  // Handle real-time validation errors from the card Element
  card.addEventListener('change', function(event) {
    var displayError = document.getElementById('card-errors');
    if (event.error) {
      displayError.textContent = event.error.message;
    } else {
      displayError.textContent = '';
    }
  });
});

 

Step 5: Create a PaymentIntent on Your Server

 

You'll need a server-side implementation to create a PaymentIntent. Here's an example in Node.js using Express:


const express = require('express');
const app = express();
const stripe = require('stripe')('sk_test_your_secret_key');

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

app.post('/create-payment-intent', async (req, res) => {
  try {
    const { amount, currency } = req.body;
    
    // Create a PaymentIntent with the order amount and currency
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: currency,
      // Enable 3D Secure by setting automatic_payment_methods parameter
      automatic_payment_methods: {
        enabled: true,
      },
    });
    
    res.send({
      clientSecret: paymentIntent.client\_secret
    });
  } catch (error) {
    res.status(500).send({ error: error.message });
  }
});

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

 

Step 6: Retrieve the PaymentIntent's Client Secret

 

In your frontend JavaScript, add a function to fetch the client secret from your server:


// Fetch the PaymentIntent's client secret
async function getClientSecret() {
  const response = await fetch('/create-payment-intent', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: 1000, // Amount in cents
      currency: 'usd',
    }),
  });
  
  const data = await response.json();
  return data.clientSecret;
}

 

Step 7: Handle Form Submission with 3D Secure Authentication

 

Update your payment form submission handler to use the client secret and handle 3D Secure authentication:


// Handle form submission
var form = document.getElementById('payment-form');
form.addEventListener('submit', async function(event) {
  event.preventDefault();
  
  // Disable the submit button to prevent repeated clicks
  document.getElementById('submit-button').disabled = true;
  
  try {
    // Get the client secret
    const clientSecret = await getClientSecret();
    
    // Confirm the payment with the card Element
    const result = await stripe.confirmCardPayment(clientSecret, {
      payment\_method: {
        card: card,
        billing\_details: {
          name: 'Customer Name', // You can get this from a form field
        },
      },
    });
    
    if (result.error) {
      // Show error to your customer
      var errorElement = document.getElementById('card-errors');
      errorElement.textContent = result.error.message;
      document.getElementById('submit-button').disabled = false;
    } else {
      // The payment succeeded!
      if (result.paymentIntent.status === 'succeeded') {
        // Show a success message to your customer
        // There's a risk of the customer closing the window before callback
        // execution. Set up a webhook or plugin to listen for the
        // payment\_intent.succeeded event that handles any business critical
        // post-payment actions.
        handlePaymentSuccess(result.paymentIntent);
      }
    }
  } catch (error) {
    console.error('Error:', error);
    document.getElementById('submit-button').disabled = false;
  }
});

function handlePaymentSuccess(paymentIntent) {
  // Redirect to a success page or update the UI
  console.log('Payment succeeded!', paymentIntent);
  // You could redirect to a success page
  // window.location.href = "/success";
  
  // Or update the UI
  var form = document.getElementById('payment-form');
  form.innerHTML = '
Payment successful!
'; }

 

Step 8: Add CSS Styling

 

Add some basic CSS to style your payment form:


.container {
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.form-row {
  margin-bottom: 20px;
}

label {
  display: block;
  margin-bottom: 10px;
  font-weight: bold;
}

#card-element {
  background-color: white;
  padding: 10px 12px;
  border-radius: 4px;
  border: 1px solid #e6e6e6;
  box-shadow: 0 1px 3px 0 #e6ebf1;
  transition: box-shadow 150ms ease;
}

#card-element--focus {
  box-shadow: 0 1px 3px 0 #cfd7df;
}

#card-element--invalid {
  border-color: #fa755a;
}

#card-errors {
  color: #fa755a;
  margin-top: 10px;
  font-size: 14px;
}

button {
  background-color: #6772e5;
  color: white;
  padding: 12px 16px;
  border-radius: 4px;
  border: 0;
  font-weight: 600;
  cursor: pointer;
  width: 100%;
  transition: all 0.2s ease;
  margin-top: 10px;
}

button:hover {
  background-color: #5469d4;
}

button:disabled {
  opacity: 0.5;
  cursor: default;
  background-color: #7795f8;
}

.success-message {
  color: #32cd32;
  font-size: 18px;
  text-align: center;
  margin: 20px 0;
}

 

Step 9: Set Up Webhook for Payment Verification

 

To properly handle payment outcomes, especially when users close the browser during 3D Secure authentication, set up a webhook:


// Add this endpoint to your server code
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = 'whsec_your_webhook_signing_secret';
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`Webhook Error: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle the event
  switch (event.type) {
    case 'payment\_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log(`PaymentIntent for ${paymentIntent.amount} was successful!`);
      // Then define and call a function to handle the successful payment intent
      // handlePaymentIntentSucceeded(paymentIntent);
      break;
    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      console.log(`Payment failed: ${failedPaymentIntent.last_payment_error?.message}`);
      // Then define and call a function to handle the failed payment intent
      // handlePaymentIntentFailed(failedPaymentIntent);
      break;
    default:
      // Unexpected event type
      console.log(`Unhandled event type ${event.type}`);
  }
  
  // Return a 200 response to acknowledge receipt of the event
  res.send();
});

 

Step 10: Test 3D Secure Authentication

 

To test 3D Secure with Stripe's test cards:

  • Use card number 4000 0000 0000 3220 for successful 3D Secure authentication
  • Use card number 4000 0000 0000 3063 for failed 3D Secure authentication
  • Use any future expiration date, any 3-digit CVC, and any 5-digit ZIP code

 

Step 11: Implement Error Handling and Loading States

 

Enhance your user experience with proper loading states and error handling:


// Add a loading state to your payment form
function setLoading(isLoading) {
  if (isLoading) {
    // Disable the button and show a spinner
    document.getElementById('submit-button').disabled = true;
    document.getElementById('submit-button').innerHTML = 'Processing...';
    // You could show a spinner here
  } else {
    document.getElementById('submit-button').disabled = false;
    document.getElementById('submit-button').innerHTML = 'Pay Now';
  }
}

// Update your form submission handler
form.addEventListener('submit', async function(event) {
  event.preventDefault();
  
  setLoading(true);
  
  try {
    // Previous code for payment confirmation
    // ...
  } catch (error) {
    console.error('Error:', error);
    var errorElement = document.getElementById('card-errors');
    errorElement.textContent = 'An unexpected error occurred. Please try again.';
  } finally {
    setLoading(false);
  }
});

 

Step 12: Handling 3D Secure on Mobile Devices

 

For better mobile experience, add responsive styling and meta tags:





  3D Secure Payment with Stripe
  
  
  


  


 

Step 13: Adding Logging for Debugging

 

Implement comprehensive logging to help with debugging:


// Client-side logging helper
function logEvent(eventName, data) {
  console.log(`Stripe Event: ${eventName}`, data);
  
  // You could also send logs to your server
  fetch('/log-event', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      event: eventName,
      data: data,
      timestamp: new Date().toISOString(),
    }),
  }).catch(err => console.error('Failed to log event:', err));
}

// Then use it in your payment flow
card.addEventListener('change', function(event) {
  logEvent('card.change', { error: event.error });
  // Rest of your handler
});

// And in your payment confirmation
const result = await stripe.confirmCardPayment(clientSecret, {
  // Options
});
logEvent('confirmCardPayment.result', { status: result.status, error: result.error });

 

Step 14: Production Considerations

 

Before going to production, implement these best practices:

  • Switch to production API keys in your Stripe initialization
  • Ensure your server uses HTTPS to secure customer data
  • Add proper error handling and recovery mechanisms
  • Implement comprehensive server-side validation
  • Set up monitoring and alerting for payment failures
  • Test thoroughly with different cards and scenarios

 

Complete Example Code

 

Here's a complete example combining all steps:


// index.html



  3D Secure Payment with Stripe
  
  
  


  

// server.js
const express = require('express');
const app = express();
const stripe = require('stripe')('sk_test_your_secret_key');

// For processing the webhook
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = 'whsec_your_webhook_signing_secret';
  
  let event;
  
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`Webhook Error: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle the event
  switch (event.type) {
    case 'payment\_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log(`PaymentIntent for ${paymentIntent.amount} was successful!`);
      // Update your database, fulfill the order, etc.
      break;
    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      console.log(`Payment failed: ${failedPaymentIntent.last_payment_error?.message}`);
      // Notify the customer, log the failure, etc.
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }
  
  res.send();
});

// For regular API requests
app.use(express.json());
app.use(express.static('public'));

app.post('/create-payment-intent', async (req, res) => {
  try {
    const { amount, currency } = req.body;
    
    // Validate inputs
    if (!amount || !currency) {
      return res.status(400).send({ error: 'Amount and currency are required' });
    }
    
    // Create a PaymentIntent
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount,
      currency: currency,
      automatic_payment_methods: {
        enabled: true,
      },
    });
    
    console.log(`Created PaymentIntent: ${paymentIntent.id}`);
    
    res.send({
      clientSecret: paymentIntent.client\_secret
    });
  } catch (error) {
    console.error('Error creating PaymentIntent:', error);
    res.status(500).send({ error: error.message });
  }
});

// Optional logging endpoint
app.post('/log-event', express.json(), (req, res) => {
  const { event, data, timestamp } = req.body;
  console.log(`[${timestamp}] ${event}:`, data);
  res.send({ success: true });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

This comprehensive tutorial covers everything you need to implement 3D Secure with Stripe Elements, from setting up your Stripe account to handling successful payments and edge cases. The implementation is secure, user-friendly, and follows best practices for payment processing.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022