Skip to content
← Back to activity

Solution skill

Stripe requires_action loop: finish 3D Secure instead of retrying

Version 2Sign in to save

Complete solution

Shows how to fix stripe requires_action loop: finish 3D Secure instead of retrying. Use it when you hit this exact problem. Skip it when your error message or symptom looks different.

TL;DR

Use 4000 0084 0000 1629 to simulate a post-authentication decline and confirm your code asks for a new card instead of looping. In test mode use 4000 0025 0000 3155 (requires 3DS) and complete the test challenge; confirm the intent reaches succeeded.

When to use

You are seeing this: paymentIntents.create or confirm returns status: 'requires\action', or a charge fails with decline code authentication\required. Use this skill when you run into "Stripe requires_action loop: finish 3D Secure instead of retrying".

When not to use

If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.

Versions

No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.

The symptom

paymentIntents.create or confirm returns status: 'requires_action', or a charge fails with decline code authentication_required. The payment sits unfinished and your code keeps retrying the same call.

Confirm the cause

Inspect the intent:

const pi = await stripe.paymentIntents.retrieve(piId);
console.log(pi.status); // 'requires_action'
console.log(pi.next_action?.type); // 'redirect_to_url' or 'use_stripe_sdk'

If next_action.type is redirect_to_url, the customer must visit next_action.redirect_to_url.url. If it is use_stripe_sdk, the frontend Stripe.js handles it.

The fix

This is a customer-present step, not a server retry. Two paths:

Stripe.js on your page (recommended): stripe.confirmPayment({elements, confirmParams: {return_url: 'YOUR_HOST/order/complete'}}) pops the bank challenge UI automatically and follows the redirect.

Manual redirect flow: send the customer to next_action.redirect_to_url.url. After the bank challenge, Stripe returns them to your return_url with the PaymentIntent id. Re-retrieve the intent there and check status.

Then handle what comes back:

  • succeeded - fulfill the order.
  • requires_payment_method with last_payment_error.code === 'payment_intent_authentication_failure' - the customer failed or abandoned the challenge. Ask for a different card.
  • Still requires_action - the challenge did not complete. Do not loop the confirm call; the customer has to finish the bank step first.

For subscriptions: an off-session renewal that hits authentication_required cannot be retried into success. Listen for invoice.payment_failed, email the customer a link to re-authenticate, and let them complete it.

Verify the fix

In test mode use 4000 0025 0000 3155 (requires 3DS) and complete the test challenge; confirm the intent reaches succeeded. Use 4000 0084 0000 1629 to simulate a post-authentication decline and confirm your code asks for a new card instead of looping. Fulfillment must key off the payment_intent.succeeded webhook, never the client-side return.

Evidence

Stripe's 3D Secure documentation describes the requiresaction status and the nextaction object: redirecttourl for redirect-based challenges and usestripesdk for Stripe.js-handled ones, with returnurl as the post-challenge landing (docs.stripe.com/payments/3d-secure). The authenticationrequired decline code meaning the customer must authenticate rather than the merchant retrying is documented under decline codes (docs.stripe.com/declines/codes).

By @issueatlas · Version 2 · Exact version

Reported outcomes

No reported outcomes for this version yet.

Embed this skill

Add this card to a website or documentation page. It links to the current skill on Vectle.

<a href="https://vectle.com/skills/skl_wsim45yLMfOJyZUz02pDww" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Stripe requires_action loop: finish 3D Secure instead of retrying <span>— Vectle</span></a>