## TL;DR

When you retry a Stripe invoice payment and get payment_intent_unexpected_state, the underlying payment intent is usually already succeeded, canceled, or requires_action, so a blind retry cannot proceed. Retrieve the intent, read its status, and branch: succeed it forward, cancel and create a new intent, or confirm the pending action. Never retry with the same intent id without checking state first.

## The query

```text
Stripe "payment_intent_unexpected_state" error on invoice payment retry
```

## Use this when

- A Stripe invoice payment retry returns payment_intent_unexpected_state
- You retry failed invoices with the stored payment intent id
- Webhook handlers re-fire payment attempts after a failure

## Not for

- A first payment attempt failed with a card decline (handle the decline itself)
- The intent is in requires_action (send the customer to complete 3DS instead)

## Steps

1. Retrieve the payment intent behind the invoice before retrying; log its status.
   Expected output: You see succeeded, canceled, or requires_payment_method, not a guess.
2. If the intent already succeeded, fetch the invoice and confirm it is marked paid.
   Expected output: The invoice shows status paid with the charge attached.
3. If the intent was canceled or requires_payment_method, create a fresh payment intent or call pay on the invoice to mint a new one.
   Expected output: A new intent id is issued in a payable state.
4. If it is in requires_action, surface the next_action URL to the customer instead of retrying.
   Expected output: The customer completes the authentication and the intent moves on.
5. Update your retry job to re-read intent state on each attempt instead of reusing a cached id.
   Expected output: Retries stop colliding with terminal states.

## Variant phrasings

### payment_intent_unexpected_state Stripe invoice retry

### Stripe retry invoice payment unexpected state error

### invoice payment intent already in terminal state Stripe

## Root cause

payment_intent_unexpected_state is a state-machine guard, not a payment failure: Stripe refuses to move an intent that has already reached a terminal state. Retrying with a stale intent id is the mismatch, and the fix is to re-derive the intent from the invoice on each attempt.

## Edge cases

- Intents can expire into canceled after abandoned requires_action sessions; check next_action expiry too
- Idempotency keys: a retried create with the same key returns the old intent instead of a fresh one, which is often how you got here

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_1myC1O1UOY4asm49QDTU1w
