Learn how to implement 3D Secure with Stripe Elements step-by-step, including setup, payment form, server code, authentication, webhooks, and best practices.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Step 1: Set Up Your Stripe Account
Before implementing 3D Secure with Stripe Elements, you need to have a Stripe account and API 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 = '
';
}
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:
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:
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.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.