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, refund / dispute / chargeback transitions, and the 3-D Secure (EMVCo 3DS 2.x) frictionless / challenge flow paths with per-gateway 3DS test cards (references/3ds-flows.md). Use when designing tests for payment flows, auditing state-handling code, or covering a 3DS round-trip; this is the state model, not a builder - to author suites on it use payment-flow-test-author (refunds, disputes, 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 references/3ds-flows.md |
| 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
3-D Secure (3DS) test flows
View source (opens in new window)3-D Secure (3DS) test flows
3-D Secure (3DS) is the EMVCo-specified card-authentication protocol. Per emvco.com (opens in new window), 3DS 2.x is the current spec (cite by stable ID: EMV 3-D Secure Protocol Specification v2.x); 3DS 1.0 is deprecated.
Use this reference when designing multi-gateway 3DS test coverage, auditing a 3DS redirect round-trip, or investigating a challenge-flow regression that is not gateway-specific.
The three flow outcomes
Per EMVCo 3DS 2.x spec:
| Outcome | Customer experience | Liability |
|---|---|---|
| Frictionless | No challenge; issuer authenticates based on risk signals | Shifts to issuer (in many regions) |
| Challenge | Customer prompted (SMS, biometric, app) | Shifts to issuer on success |
| Not applicable | 3DS not invoked (non-EU; merchant-initiated) | Stays with merchant |
The merchant's job: send all available data to the gateway; the gateway + issuer + 3DS server decide the flow.
SCA under PSD2
Per European Banking Authority RTS on SCA (opens in new window): since September 2019 (enforcement gradual through 2021), EU card payments require two-factor SCA (two of knowledge / possession / inherence). Exemptions: low-value (< €30), recurring, trusted-beneficiary, merchant-initiated - each with tracking requirements.
Per-gateway test cards
Per-PAN 3DS test cards for Stripe, Adyen, and Braintree, each with its source doc, are in gateway-test-cards.md (opens in new window). The core Stripe cards used in the assertions below: 4000 0000 0000 3055 (frictionless) and 4000 0027 6000 3184 (challenge).
Test flow surface
1. Merchant calls Authorize / PaymentIntent / Charge
2. Gateway returns "requires_action" / "RedirectShopper" / "PAYER_ACTION_REQUIRED"
per the host SKILL.md state grid
3. Frontend redirects user to issuer-hosted 3DS challenge
4. User completes challenge (or auto-completes for frictionless)
5. Redirect back to merchant return URL
6. Merchant confirms PaymentIntent (or equivalent)
7. Gateway returns final state (succeeded / failed)Test surface per step:
| Step | Test |
|---|---|
| 1 | Initiate with each test card; assert state |
| 2 | Frontend handles each gateway's "requires action" key correctly |
| 3 | Redirect URL is built with the gateway-provided client secret / token |
| 4 | Issuer-hosted page is reachable (manual + Playwright e2e) |
| 5 | Return URL handler parses the response correctly |
| 6 | Confirm call is idempotent (per the host SKILL.md Idempotency section) |
| 7 | Final state matches expected per test card |
Frictionless vs challenge - testable assertions
test('frictionless authentication', async () => {
// Card 4000 0000 0000 3055 in Stripe test mode
const intent = await stripe.paymentIntents.create({
amount: 1000, currency: 'eur',
payment_method: 'pm_card_threeDSecure2Supported',
confirm: true,
});
expect(intent.status).toBe('succeeded'); // No challenge needed
});
test('challenge flow', async () => {
// Card 4000 0027 6000 3184
const intent = await stripe.paymentIntents.create({
amount: 1000, currency: 'eur',
payment_method: 'pm_card_threeDSecure2Required',
confirm: true,
return_url: 'https://example.com/return',
});
expect(intent.status).toBe('requires_action');
expect(intent.next_action.type).toBe('redirect_to_url');
});Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Skip 3DS in tests | EU regulations require it; you'll discover broken at launch | Per-gateway 3DS card battery |
| Test only happy path | Challenge failure path also matters | Test failure + cancel mid-challenge |
| No return-URL handler test | Race conditions on redirect-back lost | Test the full round-trip |
| Hardcoded redirect URL in production code | Localhost in prod | Per-environment config |
Treat requires_action as failure | It's the normal mid-flow state | Handle explicitly |
| 3DS 1 still in code paths | Deprecated since 2022 | Remove and test |
| One test for all gateways | Each handles 3DS slightly differently | Per-gateway |
| Sync expectation on async flow | Confirm is async | Wait for webhook |
Limitations
Sources
Gateway 3DS test cards
View source (opens in new window)Gateway 3DS test cards
Per-PAN test cards that drive 3DS challenge, frictionless, and bypass flows in each gateway's test mode. Cross-referenced from 3ds-flows.md (opens in new window). For single-gateway work, prefer the gateway skills (stripe-test-cards-and-webhooks, payment-gateway-sandboxes).
Stripe
Per docs.stripe.com/testing#regulatory-cards (opens in new window):
| Card | Behaviour |
|---|---|
| 4000 0027 6000 3184 | Authentication required (challenge) |
| 4000 0025 0000 3155 | Authentication required (challenge), payment failure after success |
| 4000 0000 0000 3220 | Authentication required (challenge), payment success |
| 4000 0000 0000 3055 | 3DS supported but not required (frictionless) |
| 4242 4242 4242 4242 | Standard test card (no 3DS) |
Adyen
| Card | Behaviour |
|---|---|
| 4917 6100 0000 0000 | 3DS 2 challenge flow |
| 5454 5454 5454 5454 | 3DS 2 frictionless |
| 4012 8888 8888 1881 | 3DS 1 (deprecated; for migration testing) |
Braintree
Per developer.paypal.com/braintree/docs/guides/3d-secure/testing/node (opens in new window):
| Card | Behaviour |
|---|---|
| 4000 0000 0000 1091 | Authenticate via standard flow |
| 4000 0000 0000 1109 | Frictionless |
| 4000 0000 0000 1125 | Bypass (skipped) |
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
payment-flow-test-author
Build-an-X workflow that authors the full payment-flow test suite in three phases: the refund matrix (full / partial / multiple-partials / over-refund / already-refunded, per-gateway APIs for Stripe, Adyen, PayPal, Braintree), the chargeback / dispute suite (Visa + Mastercard reason codes, evidence submission windows, won / lost / accepted dispositions), and webhook replay + recovery via gateway-native simulators (Stripe CLI trigger / resend, Adyen Customer Area resend, PayPal Webhook Simulator, Braintree sampleNotification). Driven by the state model in payment-flow-states-reference. Use when building refund, dispute, or payment-webhook-robustness coverage for a payment integration; for generic (non-payment) webhook receiver testing use webhook-delivery-tester in the qa-notifications plugin.
payment-gateway-sandboxes
Wraps the vendor-generic payment-gateway sandbox pattern - test credentials, sandbox base URLs / environment switches, deterministic test-card matrices, and gateway-native webhook simulators - with per-gateway references for Adyen test mode, PayPal Sandbox, and Braintree sandbox. Use when testing code integrated with Adyen, PayPal, or Braintree; for Stripe use stripe-test-cards-and-webhooks (one-time payments) or stripe-subscription-billing-test-author (recurring billing).
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-flow-test-author (idempotency + replay robustness). Does not cover single-event CLI replay or handler idempotency testing (see payment-flow-test-author 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 the 3DS flows reference in payment-flow-states-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.