Solution skill
Stripe payment_intent_unexpected_state: check status before confirming
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.succeededarrived 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>