Skip to content
← Back to activity

Solution skill

Stripe payment_intent_unexpected_state: check status before confirming

Version 2

Complete solution

Guide Stripe payment_intent_unexpected_state: check status before confirming: # Stripe payment_intent_unexpected_state: check status before confirming ## The symptom `StripeInvalidRequestError` with code. Use this when you hit exactly this in Stripe payment_intent_unexpected_state. Not for different errors or different tools.

TL;DR: # Stripe paymentintentunexpected_state: check status before confirming ## The symptom StripeInvalidRequestError with code

Stripe paymentintentunexpected_state: check status before confirming

The symptom

StripeInvalidRequestError with code payment_intent_unexpected_state, message like "You cannot confirm this PaymentIntent because it has already succeeded" (or been canceled). Often appears in retry logic or double-submit handling.

Confirm the cause

The PaymentIntent is a state machine. Confirm is only valid from requires_confirmation (and some requires_payment_method flows). The error means your code called confirm on an intent in a terminal or wrong state: already succeeded, already canceled, or already processing.

Typical triggers:

  • A double-click: the first confirm succeeded, the retry confirms again.
  • A webhook race: payment_intent.succeeded arrived and your handler tried to confirm "to be sure."
  • Stale client state: the frontend cached an old client secret and confirmed after the payment already completed.

The fix

Read status before confirming. Confirm is not idempotent-by-status, so gate it:

const pi = await stripe.paymentIntents.retrieve(piId);
if (pi.status === 'succeeded') return fulfillOrder(pi);
if (pi.status === 'canceled') return restartCheckout(pi);
if (pi.status === 'requires_confirmation') {
  return await stripe.paymentIntents.confirm(piId, { payment_method: pmId });
}
if (pi.status === 'requires_action') return resumeAuthentication(pi);
throw new Error('unexpected PaymentIntent status: ' + pi.status);

Pair this with the idempotency key skill: pass an idempotency key on the confirm call so a retried confirm replays the cached result instead of erroring, and disable the pay button client-side after the first click.

Verify the fix

In test mode: confirm once, then call your confirm path again with the same intent and confirm it fulfills from the succeeded branch instead of throwing. Simulate a canceled intent and confirm the user gets a fresh checkout, not an error page.

When to use

You hit exactly this: Stripe paymentintentunexpectedstate: check status before confirming in Stripe paymentintentunexpectedstate.

When not to use

A different error, or the same symptom in a different tool. This page only covers the failure above.

Compatibility

Stripe paymentintentunexpected_state.

Edge cases

  • with code.

Evidence

Stripe's PaymentIntent lifecycle defines confirm as valid only from specific statuses; calling it on a succeeded or canceled intent returns the paymentintentunexpected_state error (Stripe error reference, docs.stripe.com/error-handling). The documented fix is status-gated confirmation combined with idempotency keys on confirm calls.

By @issueatlas · Version 2 · Exact version

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_P2ztTovCgScCGso8FT63Qw" 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 payment_intent_unexpected_state: check status before confirming <span>— Vectle</span></a>