# How to confirm a Payment Intent with Stripe

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 15 minutes
- Compatibility: Stripe.js v3+, Stripe API v2024-12+, any frontend framework
- Last updated: March 2026

## 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.

## Before you start

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

```
<form id="payment-form">
  <div id="payment-element"></div>
  <button id="submit-btn" type="submit">Pay now</button>
  <div id="error-message"></div>
</form>

<script>
const form = document.getElementById('payment-form');
form.addEventListener('submit', handleSubmit);
</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.

```
async function handleSubmit(event) {
  event.preventDefault();
  const submitBtn = document.getElementById('submit-btn');
  submitBtn.disabled = true;
  submitBtn.textContent = 'Processing...';

  const { error } = await stripe.confirmPayment({
    elements,
    confirmParams: {
      return_url: 'https://yoursite.com/payment-complete',
    },
  });

  // This code only runs if there's an immediate error
  // (e.g., card declined). For successful payments,
  // the customer is redirected to return_url.
  if (error) {
    const messageEl = document.getElementById('error-message');
    messageEl.textContent = error.message;
    submitBtn.disabled = false;
    submitBtn.textContent = 'Pay now';
  }
}
```

**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.

```
// On your /payment-complete page
const clientSecret = new URLSearchParams(window.location.search)
  .get('payment_intent_client_secret');

const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);

switch (paymentIntent.status) {
  case 'succeeded':
    showMessage('Payment succeeded!');
    break;
  case 'processing':
    showMessage('Payment is processing. We will notify you when it completes.');
    break;
  case 'requires_payment_method':
    showMessage('Payment failed. Please try another payment method.');
    break;
  default:
    showMessage('Something went wrong.');
}
```

**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.

```
const { error, paymentIntent } = await stripe.confirmPayment({
  elements,
  redirect: 'if_required',
});

if (error) {
  document.getElementById('error-message').textContent = error.message;
} else if (paymentIntent.status === 'succeeded') {
  showMessage('Payment succeeded!');
}
```

**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.

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

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

## Complete code example

File: `public/checkout.html`

```html
<!DOCTYPE html>
<html>
<head>
  <title>Payment</title>
  <script src="https://js.stripe.com/v3/"></script>
</head>
<body>
  <form id="payment-form">
    <div id="payment-element"></div>
    <button id="submit-btn" type="submit">Pay $20.00</button>
    <div id="error-message" style="color: red; margin-top: 10px;"></div>
    <div id="success-message" style="color: green; margin-top: 10px;"></div>
  </form>

  <script>
    const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');
    let elements;

    async function initialize() {
      const res = await fetch('/create-payment-intent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ amount: 2000 }),
      });
      const { clientSecret } = await res.json();

      elements = stripe.elements({ clientSecret });
      const paymentElement = elements.create('payment');
      paymentElement.mount('#payment-element');
    }

    document.getElementById('payment-form')
      .addEventListener('submit', async (e) => {
        e.preventDefault();
        const btn = document.getElementById('submit-btn');
        btn.disabled = true;
        btn.textContent = 'Processing...';

        const { error, paymentIntent } = await stripe.confirmPayment({
          elements,
          redirect: 'if_required',
        });

        if (error) {
          document.getElementById('error-message').textContent = error.message;
          btn.disabled = false;
          btn.textContent = 'Pay $20.00';
        } else if (paymentIntent.status === 'succeeded') {
          document.getElementById('success-message').textContent =
            'Payment succeeded!';
          btn.style.display = 'none';
        }
      });

    initialize();
  </script>
</body>
</html>
```

## Common mistakes

- **Not disabling the submit button during confirmation** — undefined Fix: Disable the button immediately on submit and re-enable on error. This prevents duplicate charges from double-clicks.
- **Forgetting to handle the redirect flow** — undefined Fix: stripe.confirmPayment() redirects by default for successful payments. Your return_url page must check the PaymentIntent status using the URL parameters.
- **Using confirmCardPayment instead of confirmPayment** — undefined Fix: confirmCardPayment only works with card payment methods. Use the newer confirmPayment() which works with all payment method types including cards, wallets, and bank redirects.
- **Not handling the 'processing' status** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-confirm-a-payment-intent-with-stripe
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-confirm-a-payment-intent-with-stripe
