payment-flow-states-reference
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).
Install with skills.sh (any agent)
npx skills add testland/qa --skill payment-flow-states-referencepayment-flow-states-reference
Overview
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 (opens in new window): "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.
When to use
The canonical states
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.
Authorisation vs capture
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 (opens in new window): PaymentIntent with capture_method=manual requires explicit capture call.
How to use
Worked example
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.
Idempotency
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 (opens in new window): "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.
State-handling test surface
| Surface | Test |
|---|---|
| Created → succeeded (happy path) | Standard test-card; assert each state observed |
| Requires-action (3DS) | Initiate with a challenge test card (Stripe (opens in new window) 4000 0027 6000 3184, Adyen (opens in new window) 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-patterns
| 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 |
Limitations
References
Per-platform payment state machines
View source (opens in new window)Per-platform payment state machines
Deep reference for payment-flow-states-reference SKILL.md. Consult when mapping the canonical lifecycle onto a specific provider, or when auditing that a state-handling integration covers every provider-specific state. The canonical-state names and the decision workflow stay in the SKILL; the full per-platform terminology grid, the per-provider state machines, and the async webhook / refund / dispute detail live here.
Per-platform terminology mapping
Every provider names the same lifecycle differently. This grid maps each canonical state to its provider-specific value.
| Canonical state | Stripe | Adyen | PayPal | Braintree |
|---|---|---|---|---|
| Created | requires_payment_method | Received | CREATED | created |
| Awaiting action (3DS, etc.) | requires_action | RedirectShopper | PAYER_ACTION_REQUIRED | n/a (handled inline) |
| Processing | processing | Pending | PENDING | submitted_for_settlement |
| Authorized (not captured) | requires_capture | Authorised | APPROVED (no immediate capture) | authorized |
| Captured / succeeded | succeeded | [Capture] Settled | COMPLETED | settled |
| Failed | failed (charge) | Refused | DECLINED | gateway_rejected / failed |
| Cancelled | canceled | Cancelled | VOIDED | voided |
| Refunded | succeeded + refund object | [Refund] Settled | REFUNDED | refunded |
| Disputed / chargeback | disputed (in dispute object) | [Chargeback] | dispute | disputed |
Per-platform state machines
Each provider exposes one primary object whose status field walks the lifecycle. Auth-only flows stop at the authorized state until an explicit capture; auth+capture flows run straight to the captured state.
Webhook event sequence
Providers emit one webhook per state transition; the async arrival is why tests must wait for the webhook, not the synchronous API return. Stripe example:
1. customer.created
2. payment_intent.created
3. payment_intent.requires_action (if 3DS)
4. payment_intent.processing
5. payment_intent.succeeded
AND
charge.succeededRefund states
captured payment
↓
refund created (status: pending)
↓
refund succeeded (or failed)Refunds are async: the API call returns immediately with pending, then a webhook delivers the final state minutes to hours later. Per docs.stripe.com/refunds (opens in new window).
Dispute / chargeback states
The most-complex part of the state machine. Per Visa's chargeback reason codes (cite by stable ID: Visa Chargeback Reason Codes), the customer's bank initiates the chargeback and the merchant has a fixed window to respond.
| State | Meaning |
|---|---|
| Inquiry | Bank requests info; not yet a chargeback |
| Pre-arbitration | Initial dispute filed |
| Won | Merchant evidence accepted |
| Lost | Merchant evidence rejected; funds returned to customer |
| Pre-arbitration accepted | Merchant accepts the loss |
Disputes resolve over weeks; test scenarios use test-mode dispute APIs to trigger the transitions synchronously. Per docs.stripe.com/disputes (opens in new window).
Related skills
3ds-test-flow-reference
Cross-gateway, protocol-level reference for 3-D Secure (3DS 2.x) test coverage. Covers the EMVCo frictionless / challenge / not-applicable flow paths, SCA under EU PSD2, and the per-PAN test cards for Stripe, Adyen, and Braintree. The gateway-specific wrappers (adyen-test-mode / stripe-test-cards-and-webhooks / braintree-test-cards) compose this skill for single-gateway work, so use one of those for a single-gateway query. Use this skill when designing multi-gateway 3DS test coverage, auditing a 3DS redirect round-trip, or investigating a challenge-flow regression that is not gateway-specific.
adyen-test-mode
Wraps Adyen test-mode patterns: test-API-key initialization, the canonical Adyen test cards (Visa 4111 1111 1111 1111; 5454 5454 5454 5454 3DS frictionless; 4917 6100 0000 0000 3DS 2 challenge), the per-flow result codes (Authorised / Refused / Pending / RedirectShopper / Error), the HMAC-SHA256 webhook validation, and notifications-vs-API duality (Adyen separates synchronous API calls from asynchronous notifications). Use when testing Adyen-integrated code.
braintree-test-cards
Wraps Braintree (PayPal-owned) sandbox testing patterns: sandbox merchant credentials, Drop-in / Hosted Fields client-side patterns, the Transaction lifecycle (submitted_for_settlement → settled), Braintree's distinctive test-card behaviours (specific PANs trigger specific errors), and the webhook verification (Braintree Webhook Parser). Use when testing Braintree-integrated code.
chargeback-flow-test-author
Workflow-driven skill that builds the chargeback / dispute test suite: canonical reason codes (Visa 10.4 fraud, 13.1 services-not-provided; Mastercard MCC 4855), per-gateway dispute APIs (Stripe Disputes, Adyen Chargeback notifications, PayPal Disputes), the evidence-submission flow + window, and disposition outcomes (won / lost / accepted). Use when designing dispute coverage; for refunds use refund-test-matrix-builder, for webhook redelivery + idempotency use payment-webhook-replay, and for the lifecycle state model use payment-flow-states-reference.
payment-webhook-replay
Workflow-driven skill that builds payment webhook replay + recovery tests. Covers the idempotency contract (every webhook handler must handle redelivery without side effects), the replay simulators (Stripe CLI `stripe trigger`, Adyen Customer Area resend, PayPal Webhook Simulator, Braintree webhook test), the signature-verification gauntlet (HMAC-SHA256 per gateway, expired-timestamps rejection), and the partial-failure recovery scenarios. Use when designing webhook robustness tests.
paypal-sandbox
Wraps PayPal Sandbox testing patterns: sandbox account creation (Business + Personal accounts in developer.paypal.com), the Orders v2 API (create / capture / refund), webhook event simulator (developer.paypal.com webhook simulator), sandbox-account-specific test cards, and the OAuth2 client-credentials flow for sandbox. Use when testing PayPal-integrated code.
pci-dss-scope-reference
Pure-reference catalog of PCI DSS v4.0 scope reduction techniques + the testable scope boundaries. Covers the SAQ levels (A through D, picked by how cardholder data flows), the PAN-storage prohibitions (only first-6 + last-4 retained; nothing else cleartext), the tokenization + hosted-fields scope-reduction patterns (Stripe Elements / Adyen Drop-in / Braintree Hosted Fields keep PAN off your servers), Network-Segmentation as PCI scope-reduction, and the testable behaviours the scope boundary creates. This is the catalog of WHY the boundary matters and what it makes testable - not a checker that verifies a given integration against the standard. Use when designing or auditing the PCI scope of a payment integration.
refund-test-matrix-builder
Workflow-driven skill that builds a refund test matrix from a payment-flow inventory: refund variants (full / partial / multiple-partials / over-refund / refund-on-disputed / refund-on-already-refunded), per-gateway nuances (Stripe, Adyen, PayPal, Braintree refund APIs), and timing variants (immediate / next-day / declined-by-bank), one test case per cell. Use for refund coverage on a new integration; for chargeback / dispute coverage use chargeback-flow-test-author, for webhook redelivery + idempotency use payment-webhook-replay, and for the state model use payment-flow-states-reference.
stripe-subscription-billing-test-author
Builds test suites for Stripe recurring-billing flows: trial-to-paid conversion, proration on plan upgrade and downgrade, dunning on failed renewal, cancel and reactivation, and the full subscription webhook event matrix (invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, invoice.paid). Uses Stripe Billing test clocks (POST /v1/test_helpers/test_clocks) to time-travel through billing cycles without calendar delay. Distinct from stripe-test-cards-and-webhooks (one-time PaymentIntents) and payment-webhook-replay (idempotency + replay robustness). Does not cover single-event CLI replay or handler idempotency testing (see payment-webhook-replay for those). Use when authoring tests for subscription or recurring-billing integrations.
stripe-test-cards-and-webhooks
Wraps Stripe API testing patterns: test-mode initialization, the canonical test cards (4242 success; 4000 0000 0000 0002 declined; 4000 0027 6000 3184 3DS challenge per 3ds-test-flow-reference), the Stripe CLI webhook flow (`stripe listen --forward-to`), the Stripe CLI fixture commands (`stripe trigger payment_intent.succeeded`), and the webhook signature verification (Stripe-Signature header + HMAC-SHA256). Use when testing Stripe-integrated code.