Confirm a PaymentIntent on the frontend by calling stripe.confirmPayment() with the Elements instance and a return_url. Stripe.js handles 3D Secure authentication automatically. After confirmation, check the PaymentIntent status to determine if payment succeeded or needs further action.
Confirming a PaymentIntent: The Final Step in Custom Payments
After creating a PaymentIntent on your server and mounting a PaymentElement on the frontend, the final step is confirmation. Calling stripe.confirmPayment() submits the customer's payment details, triggers any required authentication (like 3D Secure), and completes the charge. This function returns a result object — either a paymentIntent (success) or an error. You must handle both cases to provide a good user experience.
Prerequisites
- A PaymentIntent already created on your server (see 'How to Create a PaymentIntent')
- Stripe.js loaded and initialized with your publishable key (pk_test_)
- A PaymentElement mounted in your page
- A return_url for redirect-based payment methods
Step-by-step guide
Set up the payment form and submit handler
Set up the payment form and submit handler
Create an HTML form wrapping your PaymentElement and attach a submit event listener. Prevent the default form submission so Stripe.js handles it.
1<form id="payment-form">2 <div id="payment-element"></div>3 <button id="submit-btn" type="submit">Pay now</button>4 <div id="error-message"></div>5</form>67<script>8const form = document.getElementById('payment-form');9form.addEventListener('submit', handleSubmit);10</script>Expected result: The form renders with a payment element and a Pay button.
Call stripe.confirmPayment()
Call stripe.confirmPayment()
In your submit handler, call stripe.confirmPayment() with the elements instance and a return_url. For card payments, this completes immediately. For redirect-based methods (like iDEAL or bancontact), it redirects the customer to authenticate.
1async function handleSubmit(event) {2 event.preventDefault();3 const submitBtn = document.getElementById('submit-btn');4 submitBtn.disabled = true;5 submitBtn.textContent = 'Processing...';67 const { error } = await stripe.confirmPayment({8 elements,9 confirmParams: {10 return_url: 'https://yoursite.com/payment-complete',11 },12 });1314 // This code only runs if there's an immediate error15 // (e.g., card declined). For successful payments,16 // the customer is redirected to return_url.17 if (error) {18 const messageEl = document.getElementById('error-message');19 messageEl.textContent = error.message;20 submitBtn.disabled = false;21 submitBtn.textContent = 'Pay now';22 }23}Expected result: On success, the browser redirects to your return_url with payment_intent and payment_intent_client_secret query parameters.
Handle the redirect landing page
Handle the redirect landing page
On your return_url page, retrieve the PaymentIntent status using the client_secret from the URL. Show the customer an appropriate message based on the status.
1// On your /payment-complete page2const clientSecret = new URLSearchParams(window.location.search)3 .get('payment_intent_client_secret');45const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);67switch (paymentIntent.status) {8 case 'succeeded':9 showMessage('Payment succeeded!');10 break;11 case 'processing':12 showMessage('Payment is processing. We will notify you when it completes.');13 break;14 case 'requires_payment_method':15 showMessage('Payment failed. Please try another payment method.');16 break;17 default:18 showMessage('Something went wrong.');19}Expected result: The landing page displays the correct status message based on the PaymentIntent state.
Handle 3D Secure without redirect (optional)
Handle 3D Secure without redirect (optional)
If you want to stay on the same page instead of redirecting, use redirect: 'if_required'. The customer completes 3D Secure in a modal, and the promise resolves with the result.
1const { error, paymentIntent } = await stripe.confirmPayment({2 elements,3 redirect: 'if_required',4});56if (error) {7 document.getElementById('error-message').textContent = error.message;8} else if (paymentIntent.status === 'succeeded') {9 showMessage('Payment succeeded!');10}Expected result: For card payments, the promise resolves in-page with the PaymentIntent status. 3D Secure pops up as a modal.
Test with 3D Secure test cards
Test with 3D Secure test cards
Stripe provides test cards that trigger 3D Secure authentication. Use these to verify your flow handles authentication correctly.
1// Standard success (no 3DS): 4242 4242 4242 42422// 3DS required: 4000 0025 0000 31553// 3DS required, will fail: 4000 0082 6000 31784// Declined: 4000 0000 0000 0002Expected result: The 3DS test card triggers an authentication modal. Completing it successfully results in a succeeded status.
Complete working example
1<!DOCTYPE html>2<html>3<head>4 <title>Payment</title>5 <script src="https://js.stripe.com/v3/"></script>6</head>7<body>8 <form id="payment-form">9 <div id="payment-element"></div>10 <button id="submit-btn" type="submit">Pay $20.00</button>11 <div id="error-message" style="color: red; margin-top: 10px;"></div>12 <div id="success-message" style="color: green; margin-top: 10px;"></div>13 </form>1415 <script>16 const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');17 let elements;1819 async function initialize() {20 const res = await fetch('/create-payment-intent', {21 method: 'POST',22 headers: { 'Content-Type': 'application/json' },23 body: JSON.stringify({ amount: 2000 }),24 });25 const { clientSecret } = await res.json();2627 elements = stripe.elements({ clientSecret });28 const paymentElement = elements.create('payment');29 paymentElement.mount('#payment-element');30 }3132 document.getElementById('payment-form')33 .addEventListener('submit', async (e) => {34 e.preventDefault();35 const btn = document.getElementById('submit-btn');36 btn.disabled = true;37 btn.textContent = 'Processing...';3839 const { error, paymentIntent } = await stripe.confirmPayment({40 elements,41 redirect: 'if_required',42 });4344 if (error) {45 document.getElementById('error-message').textContent = error.message;46 btn.disabled = false;47 btn.textContent = 'Pay $20.00';48 } else if (paymentIntent.status === 'succeeded') {49 document.getElementById('success-message').textContent =50 'Payment succeeded!';51 btn.style.display = 'none';52 }53 });5455 initialize();56 </script>57</body>58</html>Common mistakes when confirming a Payment Intent with Stripe
Why it's a problem: Not disabling the submit button during confirmation
How to avoid: Disable the button immediately on submit and re-enable on error. This prevents duplicate charges from double-clicks.
Why it's a problem: Forgetting to handle the redirect flow
How to avoid: stripe.confirmPayment() redirects by default for successful payments. Your return_url page must check the PaymentIntent status using the URL parameters.
Why it's a problem: Using confirmCardPayment instead of confirmPayment
How to avoid: confirmCardPayment only works with card payment methods. Use the newer confirmPayment() which works with all payment method types including cards, wallets, and bank redirects.
Why it's a problem: Not handling the 'processing' status
How to avoid: Some payment methods (like ACH) can remain in 'processing' for days. Show a 'payment is being processed' message and rely on webhooks for the final status.
Best practices
- Use stripe.confirmPayment() instead of the deprecated confirmCardPayment() for forward compatibility
- Always provide a return_url even when using redirect: 'if_required' as a fallback
- Disable the submit button during processing to prevent duplicate payment attempts
- Display clear error messages from the error.message field — Stripe provides user-friendly messages
- Test with 3D Secure test cards (4000 0025 0000 3155) to verify your authentication flow
- Use webhooks (payment_intent.succeeded) as the source of truth rather than the frontend result
- Handle all PaymentIntent statuses: succeeded, processing, requires_payment_method, and requires_action
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write frontend JavaScript code that confirms a Stripe PaymentIntent using stripe.confirmPayment(). Handle both the redirect flow and the in-page flow with redirect: 'if_required'. Show error handling and success states.
Add payment confirmation to my checkout page. After mounting the Stripe PaymentElement, handle form submission with stripe.confirmPayment(). Disable the button during processing, show errors, and redirect to /payment-complete on success.
Frequently asked questions
What happens if the customer closes the browser during 3D Secure?
The PaymentIntent stays in 'requires_action' status. The customer can return to your page and try again — the same PaymentIntent is reusable. It eventually expires after 24 hours.
Should I use confirmPayment or confirmCardPayment?
Use confirmPayment(). It works with all payment methods. confirmCardPayment() is older and only works with cards. Stripe recommends the newer API for all new integrations.
How do I show a loading spinner during confirmation?
Set a loading state when the form submits and clear it when confirmPayment() returns. Since confirmPayment is async, you can use an isLoading variable to toggle the spinner visibility.
Can I confirm a PaymentIntent from the server instead?
Yes, using stripe.paymentIntents.confirm(id) on the server. This is used for off-session payments (like charging a saved card). For on-session payments, always confirm from the frontend so Stripe.js handles 3D Secure.
What is the return_url used for?
After successful payment (or authentication), Stripe redirects the customer to this URL with payment_intent and payment_intent_client_secret as query parameters. You use these to show the payment result.
Can I get help building a custom Stripe payment flow?
RapidDev specializes in building custom payment integrations. If you need help with 3D Secure flows, saved cards, or multi-step checkout, the team can help you ship faster.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation