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

How to confirm a Payment Intent with Stripe

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.

What you'll learn

  • How to confirm a PaymentIntent using stripe.confirmPayment()
  • How Stripe handles 3D Secure and SCA automatically
  • How to handle success, error, and requires_action states
  • How to redirect after payment confirmation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read15 minutesStripe.js v3+, Stripe API v2024-12+, any frontend frameworkMarch 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
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>
6
7<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.

2

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.

typescript
1async function handleSubmit(event) {
2 event.preventDefault();
3 const submitBtn = document.getElementById('submit-btn');
4 submitBtn.disabled = true;
5 submitBtn.textContent = 'Processing...';
6
7 const { error } = await stripe.confirmPayment({
8 elements,
9 confirmParams: {
10 return_url: 'https://yoursite.com/payment-complete',
11 },
12 });
13
14 // This code only runs if there's an immediate error
15 // (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.

3

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.

typescript
1// On your /payment-complete page
2const clientSecret = new URLSearchParams(window.location.search)
3 .get('payment_intent_client_secret');
4
5const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
6
7switch (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.

4

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.

typescript
1const { error, paymentIntent } = await stripe.confirmPayment({
2 elements,
3 redirect: 'if_required',
4});
5
6if (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.

5

Test with 3D Secure test cards

Stripe provides test cards that trigger 3D Secure authentication. Use these to verify your flow handles authentication correctly.

typescript
1// Standard success (no 3DS): 4242 4242 4242 4242
2// 3DS required: 4000 0025 0000 3155
3// 3DS required, will fail: 4000 0082 6000 3178
4// Declined: 4000 0000 0000 0002

Expected result: The 3DS test card triggers an authentication modal. Completing it successfully results in a succeeded status.

Complete working example

public/checkout.html
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>
14
15 <script>
16 const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');
17 let elements;
18
19 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();
26
27 elements = stripe.elements({ clientSecret });
28 const paymentElement = elements.create('payment');
29 paymentElement.mount('#payment-element');
30 }
31
32 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...';
38
39 const { error, paymentIntent } = await stripe.confirmPayment({
40 elements,
41 redirect: 'if_required',
42 });
43
44 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 });
54
55 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.

ChatGPT Prompt

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.

Stripe Prompt

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.

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.