Stripe “Payment Intent” stuck in “Requires Action” status

A Stripe PaymentIntent stuck in the requires_action status indicates that the payment process has paused because the customer’s card issuer or payment method requires additional authentication before funds can be authorized. In most cases, this occurs when Strong Customer Authentication (SCA), 3D Secure (3DS), or a localized banking redirect is triggered, but the frontend application fails to present or complete the challenge. To fix a PaymentIntent halted in this state, your client-side code must handle the next_action payload by invoking Stripe’s confirmation helpers, or your backend must clean up abandoned sessions when a user drops off during authentication.

What does it mean when a Stripe PaymentIntent is in “requires_action”?

A PaymentIntent enters requires_action when the card issuer demands customer verification—such as a 3D Secure one-time passcode or biometric approval—before authorizing the charge. The payment remains in this state until the frontend handles the challenge using stripe.handleNextAction or the customer abandons the session.


Diagnostic Snapshot: PaymentIntent requires_action

ParameterDiagnostic Value
Current Statuspayment_intent.status == "requires_action"
Typical Next Actionuse_stripe_sdk, redirect_to_url, or verify_with_microdeposits
ImpactPayment is not authorized; no funds are captured
Primary TriggerStrong Customer Authentication (SCA) / 3D Secure mandate
Where to InspectStripe Dashboard > Payments > [Payment ID] > History

Diagnostic Path: Tracing Stalled Confirmations

PaymentIntent status is "requires_action"
  │
  ├──► Check Dashboard: Developers > Logs > /v1/payment_intents/:id
  │      │
  │      ├── Next Action Type: "use_stripe_sdk"
  │      │     └── Did frontend call stripe.handleNextAction() or confirmCardPayment()?
  │      │           ├── NO:  Frontend dropped the authentication promise
  │      │           └── YES: Customer closed the 3DS challenge modal / abandoned tab
  │      │
  │      ├── Next Action Type: "redirect_to_url"
  │      │     └── Customer failed to complete external bank redirect (iDEAL, Sofort, etc.)
  │      │
  │      └── Next Action Type: "verify_with_microdeposits"
  │            └── ACH / Direct Debit awaiting manual account amount verification
  │
  └──► Check Payment Context
         └── Is this an off-session recurring charge?
               └── Off-session charge cannot complete 3DS without sending customer an auth link

Why a PaymentIntent Enters “Requires Action”

The PaymentIntent API follows a strict state machine:

requires_payment_method ──► requires_confirmation ──► requires_action ──► processing ──► succeeded

When you or your customer submits payment details, Stripe contacts the issuing bank. Under regulations like the European Revised Payment Services Directive (PSD2) and global card network fraud standards, banks often mandate Strong Customer Authentication.

If the bank requires verification, Stripe halts the state machine at requires_action and populates the next_action object in the API response:

{
  "id": "pi_3NabcXYZ",
  "status": "requires_action",
  "next_action": {
    "type": "use_stripe_sdk",
    "use_stripe_sdk": {
      "type": "three_d_secure_redirect",
      "stripe_js": "<https://hooks.stripe.com/redirect/>...",
      "source": "src_12345"
    }
  }
}

The PaymentIntent is not failed and it is not charged. It is waiting for the client to complete the instruction detailed in next_action. If the customer closes the browser, loses internet connectivity, or your frontend code ignores this status, the PaymentIntent sits in requires_action until it expires (typically after 23 hours).


Common Causes of Stalled PaymentIntents

1. Incomplete Client-Side Authentication Handling

If you confirm the PaymentIntent on your application server using stripe.paymentIntents.confirm(), the server receives the requires_action response. If your backend simply checks for status === 'succeeded' and returns an error when it sees any other state, the client is never instructed to display the 3D Secure authentication modal.

2. Customer Drop-off During 3D Secure

Many customers simply abandon transactions when confronted with an SMS verification code or banking app approval prompt. When the customer closes the tab or hits the back button, the client-side JavaScript execution halts. Because no decline was sent by the cardholder, the PaymentIntent remains in requires_action until it reaches its automatic expiration timeout.

If legitimate transactions are being routed into aggressive fraud challenges unnecessarily, verify your rules against Fix: Stripe “Radar” False Positives – Rescuing Blocked Payments.

3. Attempting Off-Session Charges on SCA-Mandated Cards

If you run a recurring billing engine or off-cycle invoice payment using a saved card, you must pass off_session: true.

If the bank demands 3DS authentication for an off-session attempt, the charge cannot complete because no human is present to enter the code. The PaymentIntent transitions to requires_action. If your system does not detect this and alert the customer via email, the transaction stalls indefinitely. If card declines occur regularly during these handoffs, consult Fix: Stripe “402 Request Failed” (Card Declined Deep Dive).


Step-by-Step Resolution

Step 1: Implement Proper Client-Side Confirmation

If using Stripe.js, do not confirm sensitive payments solely on the backend. Use the client-side SDK helpers, which automatically intercept requires_action and open the bank’s verification modal.

Frontend implementation using Stripe Elements:

const { error, paymentIntent } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: '<https://example.com/order/complete>',
  },
});

if (error) {
  // Show error to customer (e.g., card declined)
  showError(error.message);
} else if (paymentIntent && paymentIntent.status === 'requires_action') {
  // Stripe.js automatically handles standard 3DS modals, but if you handle
  // actions manually from a server-confirmed intent, use handleNextAction:
  const result = await stripe.handleNextAction({
    clientSecret: paymentIntent.client_secret
  });

  if (result.error) {
    showError(result.error.message);
  } else {
    window.location.href = '/order/complete';
  }
}

Step 2: Ensure Return URL Redirection is Resilient

When the customer completes 3D Secure in a separate browser tab or banking app, Stripe redirects them to your declared return_url.

Ensure that your return_url page inspects the incoming query parameters:

  • payment_intent_client_secret
  • payment_intent

On page load, retrieve the PaymentIntent status using stripe.retrievePaymentIntent(clientSecret) to display the final confirmation screen, preventing infinite redirection loops described in Fix: Stripe “Post-payment” redirect loop to Success Page.

If an automated background charge hits requires_action, catch the error on your server and notify the customer:

try {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 2900,
    currency: 'usd',
    customer: customerId,
    payment_method: savedPaymentMethodId,
    off_session: true,
    confirm: true,
  });
} catch (err) {
  if (err.raw && err.raw.payment_intent.status === 'requires_action') {
    const pi = err.raw.payment_intent;

    // Send an authentication email to the customer
    await sendScaRecoveryEmail(customer.email, {
      paymentIntentId: pi.id,
      clientSecret: pi.client_secret,
    });
  }
}

Direct the customer to a hosted page containing Stripe Elements that calls stripe.handleNextAction(clientSecret) to complete the bank challenge while the customer is active.

Step 4: Handle Abandoned PaymentIntents

Because customers will occasionally abandon 3DS prompts, your order database must not mark orders as failed or fulfilled while in requires_action.

  1. When a PaymentIntent enters requires_action, keep the internal order status as pending_payment.
  2. Rely strictly on webhooks for fulfillment:
    • payment_intent.succeeded: Fulfill the order.
    • payment_intent.payment_failed or payment_intent.canceled: Cancel the order and release inventory locks.
  3. If duplicate charges appear when customers attempt to restart abandoned checkouts, protect your endpoints using Fix: Stripe “Idempotency Key” reuse causing Duplicate Charges.