Learn how to handle SCA (Strong Customer Authentication) in Stripe with step-by-step guidance on setup, Payment Intents, 3D Secure, off-session payments, and subscriptions.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
How to Handle SCA (Strong Customer Authentication) in Stripe
Step 1: Understanding SCA Requirements
Strong Customer Authentication (SCA) is a European regulatory requirement to reduce fraud and make online payments more secure. It requires authentication using at least two of the following:
In Stripe, this typically means implementing 3D Secure authentication for card payments.
Step 2: Setting Up Your Stripe Account
Before implementing SCA, ensure your Stripe account is properly configured:
For Node.js:
npm install stripe
For PHP:
composer require stripe/stripe-php
Step 3: Implementing the Payment Intent API
The Payment Intent API is central to handling SCA. It manages the payment flow and handles authentication when needed:
// Node.js
const stripe = require('stripe')('sk_test_your\_key');
async function createPaymentIntent(amount, currency) {
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: currency,
// Automatically handles SCA requirements when needed
automatic_payment_methods: { enabled: true }
});
return paymentIntent;
}
Step 4: Client-Side Implementation with Stripe Elements
Implement Stripe Elements on your frontend to securely collect payment details:
<!-- HTML structure -->
<form id="payment-form">
<div id="payment-element"></div>
<button id="submit">Pay now</button>
<div id="error-message"></div>
</form>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('pk_test_your\_key');
// Fetch the PaymentIntent client secret from your server
fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000, currency: 'eur' })
})
.then(response => response.json())
.then(data => {
const { clientSecret } = data;
const elements = stripe.elements({ clientSecret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Handle form submission
const form = document.getElementById('payment-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return\_url: 'https://example.com/payment-complete',
},
});
if (error) {
const messageElement = document.getElementById('error-message');
messageElement.textContent = error.message;
}
});
});
</script>
Step 5: Creating a Server Endpoint for PaymentIntent
Create a server endpoint to generate a PaymentIntent and return the client secret:
// Node.js with Express
const express = require('express');
const app = express();
const stripe = require('stripe')('sk_test_your\_key');
app.use(express.json());
app.post('/create-payment-intent', async (req, res) => {
try {
const { amount, currency } = req.body;
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
automatic_payment_methods: { enabled: true },
});
res.json({ clientSecret: paymentIntent.client\_secret });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Step 6: Handling the Return from 3D Secure Authentication
When SCA is required, the customer will be redirected to complete 3D Secure authentication. Set up a page to handle the return:
<!-- payment-complete.html -->
<div id="status-container">Processing payment...</div>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('pk_test_your\_key');
// Extract the client secret from the URL query parameters
const clientSecret = new URLSearchParams(window.location.search).get('payment_intent_client\_secret');
if (clientSecret) {
stripe.retrievePaymentIntent(clientSecret)
.then(({ paymentIntent }) => {
const statusContainer = document.getElementById('status-container');
// Handle payment status
switch (paymentIntent.status) {
case 'succeeded':
statusContainer.textContent = 'Payment succeeded!';
// Handle successful payment
break;
case 'processing':
statusContainer.textContent = 'Payment processing.';
// Handle processing payment
break;
case 'requires_payment_method':
statusContainer.textContent = 'Payment failed. Please try another payment method.';
// Handle failed payment
break;
default:
statusContainer.textContent = `Unexpected status: ${paymentIntent.status}`;
}
});
}
</script>
Step 7: Implementing Off-Session Payments with Setup Intents
For recurring payments or future off-session charges that might require SCA, use Setup Intents to save payment methods:
// Server-side: Create a Setup Intent
const setupIntent = await stripe.setupIntents.create({
payment_method_types: ['card'],
customer: 'cus_customer_id', // Attach to a customer
});
// Return client\_secret to the frontend
res.json({ clientSecret: setupIntent.client\_secret });
Client-side implementation:
<script>
const stripe = Stripe('pk_test_your\_key');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const form = document.getElementById('setup-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret,
{
payment\_method: {
card: cardElement,
billing\_details: { name: 'Customer Name' },
},
}
);
if (error) {
// Handle error
console.error(error);
} else {
// The card has been successfully set up
console.log('Success! Payment method ID:', setupIntent.payment\_method);
// Send the payment method ID to your server to save for future payments
savePaymentMethod(setupIntent.payment\_method);
}
});
function savePaymentMethod(paymentMethodId) {
// Send to your server
fetch('/save-payment-method', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentMethodId }),
});
}
</script>
Step 8: Making Off-Session Charges with SCA Handling
When charging a saved payment method off-session, handle potential SCA requirements:
// Server-side code
async function chargeCustomerOffSession(customerId, paymentMethodId, amount) {
try {
// Create a PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'eur',
customer: customerId,
payment\_method: paymentMethodId,
off\_session: true,
confirm: true,
});
// Payment succeeded without SCA
return { success: true, paymentIntent };
} catch (error) {
// Error code for authentication required
if (error.code === 'authentication\_required') {
// Create a new PaymentIntent for on-session authentication
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'eur',
customer: customerId,
payment\_method: paymentMethodId,
confirm: false, // Don't confirm yet
setup_future_usage: 'off\_session',
});
// Return the client secret to complete authentication
return {
success: false,
requires\_action: true,
client_secret: paymentIntent.client_secret,
};
}
// Handle other errors
return { success: false, error: error.message };
}
}
Step 9: Handling SCA Exceptions and Exemptions
Some transactions may be exempt from SCA. Stripe automatically applies for exemptions when possible:
// Request an exemption when creating a PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'eur',
payment_method_types: ['card'],
// Apply for low-value exemption if eligible (transaction < €30)
setup_future_usage: 'off\_session',
});
Step 10: Testing SCA Implementation
Use Stripe's test cards to verify your SCA implementation:
Test code:
// Test with a card requiring authentication
const testSCA = async () => {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: 1999,
currency: 'eur',
payment_method_types: ['card'],
payment_method: 'pm_card\_threeDSecure2Required', // Test SCA card
confirm: true,
});
console.log('Authentication status:', paymentIntent.status);
// Should return 'requires\_action'
} catch (error) {
console.error('Error:', error);
}
};
Step 11: Monitoring and Handling Payment Status
Set up webhook endpoints to monitor payment status and handle events:
// Node.js with Express
const endpointSecret = 'whsec_your_webhook\_secret';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
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 was successful!', paymentIntent.id);
// Update your database, fulfill the order, etc.
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
console.log('Payment failed:', failedPayment.id, failedPayment.last_payment_error?.message);
// Notify the customer, retry payment, etc.
break;
case 'payment_intent.requires_action':
const requiresAction = event.data.object;
console.log('Payment requires authentication:', requiresAction.id);
// Notify the customer to complete authentication
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
res.send();
});
Step 12: Implementing a Complete Payment Flow with SCA
Here's a complete example of a payment flow handling SCA:
// Server-side (Node.js with Express)
const express = require('express');
const app = express();
const stripe = require('stripe')('sk_test_your\_key');
app.use(express.static('public'));
app.use(express.json());
// Create PaymentIntent
app.post('/create-payment', async (req, res) => {
try {
const { amount, currency, customer\_email } = req.body;
// Create or retrieve customer
let customer;
const customers = await stripe.customers.list({
email: customer\_email,
limit: 1,
});
if (customers.data.length > 0) {
customer = customers.data[0];
} else {
customer = await stripe.customers.create({
email: customer\_email,
});
}
// Create PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
customer: customer.id,
setup_future_usage: 'off\_session', // Save for future use
automatic_payment_methods: { enabled: true }
});
res.json({
clientSecret: paymentIntent.client\_secret,
customerId: customer.id
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Save payment method for future use
app.post('/save-payment-method', async (req, res) => {
try {
const { customerId, paymentMethodId } = req.body;
// Attach payment method to customer
await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
});
// Set as default payment method
await stripe.customers.update(customerId, {
invoice\_settings: {
default_payment_method: paymentMethodId,
},
});
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Make off-session payment
app.post('/charge-customer', async (req, res) => {
try {
const { customerId, amount, currency } = req.body;
// Get customer's default payment method
const customer = await stripe.customers.retrieve(customerId);
const paymentMethodId = customer.invoice_settings.default_payment\_method;
if (!paymentMethodId) {
return res.status(400).json({ error: 'No payment method found' });
}
try {
// Attempt to charge without authentication
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
customer: customerId,
payment\_method: paymentMethodId,
off\_session: true,
confirm: true,
});
res.json({ success: true, paymentIntent });
} catch (error) {
if (error.code === 'authentication\_required') {
// SCA required, create a new PaymentIntent for on-session authentication
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
customer: customerId,
payment\_method: paymentMethodId,
setup_future_usage: 'off\_session',
});
res.json({
success: false,
requires\_action: true,
client_secret: paymentIntent.client_secret,
});
} else {
throw error;
}
}
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Client-side implementation:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SCA Payment Example</title>
<script src="https://js.stripe.com/v3/"></script>
<style>
.form-group { margin-bottom: 20px; }
#payment-element { margin: 20px 0; }
.hidden { display: none; }
</style>
</head>
<body>
<h1>Checkout with SCA</h1>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" required>
</div>
<div class="form-group">
<label for="amount">Amount (in cents):</label>
<input type="number" id="amount" value="1999" required>
</div>
<form id="payment-form">
<div id="payment-element"></div>
<button type="submit" id="submit-button">Pay now</button>
<div id="error-message"></div>
</form>
<div id="success-message" class="hidden">
<h2>Payment successful!</h2>
<p>Your payment was processed successfully.</p>
<div class="form-group">
<button id="pay-again-button">Make another payment</button>
</div>
</div>
<script>
const stripe = Stripe('pk_test_your\_key');
let elements;
let customerId;
// Initialize the payment form
async function initializePayment() {
const emailInput = document.getElementById('email');
const amountInput = document.getElementById('amount');
// Validate inputs
if (!emailInput.value || !amountInput.value) {
alert('Please enter email and amount');
return;
}
try {
// Create PaymentIntent on the server
const response = await fetch('/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: parseInt(amountInput.value),
currency: 'eur',
customer\_email: emailInput.value
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const { clientSecret, customerId: custId } = data;
customerId = custId;
// Initialize Stripe Elements
elements = stripe.elements({ clientSecret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Handle form submission
const form = document.getElementById('payment-form');
form.addEventListener('submit', handleSubmit);
document.getElementById('payment-form').classList.remove('hidden');
} catch (error) {
const errorMsg = document.getElementById('error-message');
errorMsg.textContent = error.message;
}
}
// Handle the payment form submission
async function handleSubmit(e) {
e.preventDefault();
const submitButton = document.getElementById('submit-button');
submitButton.disabled = true;
// Confirm the payment
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return\_url: window.location.origin + '/payment-complete.html',
},
redirect: 'if\_required'
});
if (error) {
// Show error message
const errorMsg = document.getElementById('error-message');
errorMsg.textContent = error.message;
submitButton.disabled = false;
} else {
// Payment succeeded without 3D Secure redirect
handlePaymentSuccess();
}
}
// Handle successful payment
function handlePaymentSuccess() {
document.getElementById('payment-form').classList.add('hidden');
document.getElementById('success-message').classList.remove('hidden');
// Enable "Pay Again" button
document.getElementById('pay-again-button').addEventListener('click', () => {
document.getElementById('success-message').classList.add('hidden');
document.getElementById('payment-form').classList.remove('hidden');
initializePayment();
});
}
// Initialize page
document.addEventListener('DOMContentLoaded', () => {
const submitButton = document.getElementById('submit-button');
submitButton.addEventListener('click', () => {
if (!elements) {
initializePayment();
}
});
// Check for redirect from 3D Secure
const urlParams = new URLSearchParams(window.location.search);
const paymentIntentClientSecret = urlParams.get('payment_intent_client\_secret');
if (paymentIntentClientSecret) {
stripe.retrievePaymentIntent(paymentIntentClientSecret)
.then(({ paymentIntent }) => {
if (paymentIntent.status === 'succeeded') {
handlePaymentSuccess();
}
});
}
});
</script>
</body>
</html>
Step 13: Implementing SCA for Subscriptions
For subscription payments that require SCA:
// Server-side code
app.post('/create-subscription', async (req, res) => {
const { customerId, priceId, paymentMethodId } = req.body;
try {
// Set the default payment method on the customer
await stripe.customers.update(customerId, {
invoice\_settings: {
default_payment_method: paymentMethodId,
},
});
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
expand: ['latest_invoice.payment_intent'],
});
const status = subscription.latest_invoice.payment_intent.status;
const clientSecret = subscription.latest_invoice.payment_intent.client\_secret;
res.json({
subscriptionId: subscription.id,
status,
clientSecret: status === 'requires\_action' ? clientSecret : null,
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Client-side subscription handling:
<script>
async function createSubscription(priceId) {
try {
// Create subscription on server
const response = await fetch('/create-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: customerId,
priceId: priceId,
paymentMethodId: paymentMethodId
})
});
const subscription = await response.json();
// If subscription needs SCA
if (subscription.status === 'requires\_action') {
const { error, paymentIntent } = await stripe.confirmCardPayment(
subscription.clientSecret
);
if (error) {
// Handle error
throw new Error(error.message);
} else {
// Subscription active
alert('Subscription activated successfully!');
}
} else {
// Subscription active
alert('Subscription activated successfully!');
}
} catch (error) {
alert('Error: ' + error.message);
}
}
</script>
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.