Pure-reference catalog of payment lifecycle state machines across Stripe, Adyen, PayPal, and Braintree: canonical states (created / requires_action / processing / succeeded / requires_capture / canceled / failed), authorisation vs capture, asynchronous webhook states, and refund / dispute / chargeback transitions. Use when designing tests for payment flows or auditing state-handling code; this is the state model, not a builder - to author suites on it use refund-test-matrix-builder (refunds), chargeback-flow-test-author (disputes), or payment-webhook-replay (webhook replay).
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Medium
Suggest reviewing before use
Every payment platform exposes a state machine - the PaymentIntent in Stripe, the Authorisation in Adyen, the Order in PayPal, the Transaction in Braintree. Each has different terminology for what is fundamentally the same lifecycle.
Per stripe.com/docs/payments/payment-intents: "The PaymentIntent encapsulates the lifecycle of a customer payment."
The full per-platform terminology grid, the per-provider state machines, and the async webhook / refund / dispute detail live in references/payment-state-machines.md.
Most payment systems share the same conceptual lifecycle. The canonical states, in order:
Each provider names these differently. The full canonical-to-provider grid and the four per-provider state machines (Stripe PaymentIntent, Adyen Authorisation/Capture, PayPal Order, Braintree Transaction) are in references/payment-state-machines.md.
Two-step:
Default in most systems is auto-capture (auth + capture in one call). Separate auth-then-capture is used for:
Per stripe.com/docs/payments/capture:
PaymentIntent with capture_method=manual requires explicit capture call.
Map a Stripe PaymentIntent lifecycle for a 3DS card that then gets
refunded. The path: requires_payment_method -> requires_action ->
processing -> succeeded, then a refund pending -> succeeded.
Test cases derived, one per transition:
| Transition | Test case |
|---|---|
requires_payment_method -> requires_action | Confirm with a challenge card; assert requires_action + next_action.type = redirect_to_url |
requires_action -> processing | Complete the issuer challenge; assert the intent leaves requires_action |
processing -> succeeded | Wait for payment_intent.succeeded webhook; assert final state (not the sync return) |
succeeded -> refund pending | Issue a full refund; assert refund object pending |
refund pending -> succeeded | Wait for charge.refunded webhook; assert refund succeeded |
Each async assertion waits on the webhook, so the same lifecycle exercised
without 3DS (a frictionless card that skips requires_action) is a separate
case, not a variant of this one.
Most payment APIs accept an Idempotency-Key header (Stripe, Adyen) or
equivalent. The pattern: retry with the same key produces the same response.
Per stripe.com/docs/api/idempotent_requests: "Stripe supports idempotency for safely retrying requests without accidentally performing the same operation twice."
Tests should verify the merchant code uses idempotency keys for every mutating call.
| Surface | Test |
|---|---|
| Created → succeeded (happy path) | Standard test-card; assert each state observed |
| Requires-action (3DS) | Initiate with a challenge test card (Stripe 4000 0027 6000 3184, Adyen 4917 6100 0000 0000); assert requires_action / RedirectShopper with next_action.type = redirect_to_url; complete the issuer-hosted challenge; confirm and assert succeeded. Repeat with a frictionless card (Stripe 4000 0000 0000 3055) - must reach succeeded with no challenge. Per 3ds-test-flow-reference |
| Failed (insufficient funds) | Use insufficient-funds test card; assert state |
| Cancelled before capture | Manual-capture + cancel; assert state |
| Webhook idempotency | Replay webhook twice; assert idempotent handling |
| Refund full | Capture + full refund; assert state sequence |
| Refund partial | Capture + partial refund; assert state |
| Dispute won | Trigger dispute; respond; assert won |
| Dispute lost | Trigger dispute; don't respond; assert lost |
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Treating the API return as the final state | Async; succeeded comes later | Wait for webhook |
| No idempotency key | Network retries duplicate-charge customers | Always set idempotency |
| Hardcoded sleep waiting for webhooks | Flaky | Poll webhook endpoint or queue with timeout |
| Skipping the requires-action flow | 3DS regulations require it for most EU cards | Always test 3DS path |
| Stale state stored locally | Local DB diverges from platform | Webhook-driven update |
| Trust the request-body status | Webhooks can be replayed by attackers | Verify signature + idempotency |
| One test for all platforms | State terminology differs | Per-platform test suite |
| Refund tests in sync flow | Refunds are async | Webhook-based |
setup_intent for saved payment
methods; PayPal's Orders API is newer than the legacy Payments API.3ds-test-flow-reference,
pci-dss-scope-reference.stripe-test-cards-and-webhooks,
adyen-test-mode,
paypal-sandbox,
braintree-test-cards,
refund-test-matrix-builder,
chargeback-flow-test-author,
payment-webhook-replay.