Skip to content

Payment Lifecycle

┌──────────────────────────┐
│ requires_payment_method │◄── (created)
└─────────────┬────────────┘
│ payment method attached
▼
┌──────────────────────────┐
│ requires_confirmation │
└─────────────┬────────────┘
│ confirm called
┌────────────┴─────────────────────┐
│ │
│ scheduled_payment_date │ no schedule / date = today
│ is in the future │
▼ ▼
┌──────────────┐ ┌──────────────────┴──────────────┐
│ ready │ │ │
│ (scheduled) │ ▼ ▼
└──────┬───────┘ ┌────────────────┐ ┌────────────────────────┐
│ │ processing │ │ requires_action │
process_at │ │ (PSP pending) │ │ (3DS challenge) │
reached │ └───────┬────────┘ └────────────┬───────────┘
│ │ │
▼ ┌──────┴──────┐ ┌──────┴──────┐
(joins processing ▼ ▼ ▼ ▼
flow above) ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│succeeded │ │ failed │ │succeeded │ │ failed │
└─────┬────┘ └──────────┘ └─────┬────┘ └──────────┘
│ full refund │ full refund
▼ ▼
┌──────────┐ ┌──────────┐
│ refunded │ │ refunded │
└──────────┘ └──────────┘
Any non-terminal state → canceled (terminal)

The initial state for all payment intents created via the API. No payment method has been attached. The intent stays here until a payment_method is provided at confirm time.

A payment method has been attached but confirm has not been called. In most flows this state is transient — you attach the payment method and call confirm in the same step.

The payment intent has been confirmed with a future scheduled_payment_date. No money moves yet. The intent is queued — on the scheduled date, EnqueueScheduledPaymentIntentsJob picks it up and submits it for processing.

A ready intent can still be canceled before its scheduled date.

Canceling a payment that belongs to a payment plan skips that date, as Skip does in the dashboard. It’s refused with 400 invalid_status_for_cancel when the payment is dated today or earlier on your account’s clock, falls inside one of the plan’s suspension periods, belongs to a plan that is closing or closed, or has been claimed or sent for processing. Otherwise the payment is removed and the plan schedules again: a plan with a total collects the amount at the end. A refused cancel changes nothing and sends no payment_intent.canceled event. See Canceling a plan’s payment.

See Scheduled Payments for details on creating scheduled intents.

The payment requires an additional customer action, typically a 3D Secure authentication challenge. The widget handles this automatically. The intent transitions to processing after the action completes, or failed if the customer abandons the challenge.

The payment has been submitted to the PSP and is awaiting a final result. This state can last from milliseconds to several business days depending on the payment method (a bank debit clears about 2 business days after it’s sent).

If a card payment’s result doesn’t reach us, it stays processing while we confirm with the PSP whether the customer was charged. It then ends succeeded, or failed with processing_timeout. It isn’t charged twice in the meantime: a repeat confirm is refused until it’s settled.

The payment completed successfully. Terminal state — the customer has been charged. Partial refunds keep the intent in succeeded; only a full refund transitions to refunded.

All captured funds have been refunded. The intent transitions here automatically when the total refunded amount equals the original charge. Terminal state.

The payment was declined or failed for another reason. Check the failure code for details. Terminal state — create a new payment intent to retry.

Two codes in last_payment_error mean the failure was on our side, not the card issuer’s. The customer wasn’t charged and their card is unaffected, so it’s safe to try again:

Code Meaning
processing_error The payment couldn’t be sent to the PSP because of a problem on our side
processing_timeout The payment timed out on our side and was cancelled

The payment intent was explicitly canceled. Terminal state.

  • Use webhooks, not polling. The synchronous confirm response may show processing or ready — do not assume success until you receive payment_intent.succeeded.
  • succeeded is authoritative. Build your fulfillment logic on this webhook event.
  • failed is permanent. Create a new payment intent to retry a failed payment.
  • ready is not charged yet. A ready response means the payment is scheduled, not collected. Monitor payment_intent.succeeded to confirm funds were captured.

After a payment intent reaches succeeded, refunds are tracked as separate objects. A partial refund keeps the payment intent in succeeded. A full refund (total refunded equals original charge) transitions the payment intent to refunded. Use the refund object’s status (succeeded, pending, failed) and the payment intent’s status to track state.