Testland
Browse all skills & agents

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-reference
View source

payment-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

  • Designing a payment-flow test suite.
  • Auditing state-handling code for a payment integration.
  • Mapping equivalent states across multiple providers.
  • Investigating "stuck payment" reports.

The canonical states

Most payment systems share the same conceptual lifecycle. The canonical states, in order:

  1. Created - intent exists, no payment method confirmed yet.
  2. Awaiting action - a 3DS or redirect challenge is pending.
  3. Processing - submitted, awaiting the async result.
  4. Authorized (not captured) - funds reserved; auth-only flows stop here.
  5. Captured / succeeded - funds transferred to the merchant.
  6. Failed - declined or rejected.
  7. Cancelled - voided before capture.
  8. Refunded - captured then reversed (async).
  9. Disputed / chargeback - customer's bank pulled the funds.

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:

  1. Authorize - bank reserves funds; merchant doesn't get them yet.
  2. Capture - funds transferred to merchant.

Default in most systems is auto-capture (auth + capture in one call). Separate auth-then-capture is used for:

  • Hold-then-charge flows (rental cars, hotels).
  • Inventory-confirm-before-charge.
  • Manual fraud review.

Per stripe.com/docs/payments/capture (opens in new window): PaymentIntent with capture_method=manual requires explicit capture call.

How to use

  1. Identify the platform and flow - which provider, and whether it is auth-only or auth+capture (see Authorisation vs capture).
  2. Map its state machine from references/payment-state-machines.md - translate the canonical states into that provider's terminology.
  3. Enumerate the async transitions - the webhook states plus the refund, dispute, and chargeback transitions the flow can reach.
  4. Derive test cases per transition - one case per edge, happy and off-path (see State-handling test surface).
  5. Assert the state-handling code covers each - every transition the provider can emit has a handler and is webhook-driven, not inferred from the synchronous API return.

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:

TransitionTest case
requires_payment_method -> requires_actionConfirm with a challenge card; assert requires_action + next_action.type = redirect_to_url
requires_action -> processingComplete the issuer challenge; assert the intent leaves requires_action
processing -> succeededWait for payment_intent.succeeded webhook; assert final state (not the sync return)
succeeded -> refund pendingIssue a full refund; assert refund object pending
refund pending -> succeededWait 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

SurfaceTest
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 captureManual-capture + cancel; assert state
Webhook idempotencyReplay webhook twice; assert idempotent handling
Refund fullCapture + full refund; assert state sequence
Refund partialCapture + partial refund; assert state
Dispute wonTrigger dispute; respond; assert won
Dispute lostTrigger dispute; don't respond; assert lost

Anti-patterns

Anti-patternWhy it failsFix
Treating the API return as the final stateAsync; succeeded comes laterWait for webhook
No idempotency keyNetwork retries duplicate-charge customersAlways set idempotency
Hardcoded sleep waiting for webhooksFlakyPoll webhook endpoint or queue with timeout
Skipping the requires-action flow3DS regulations require it for most EU cardsAlways test 3DS path
Stale state stored locallyLocal DB diverges from platformWebhook-driven update
Trust the request-body statusWebhooks can be replayed by attackersVerify signature + idempotency
One test for all platformsState terminology differsPer-platform test suite
Refund tests in sync flowRefunds are asyncWebhook-based

Limitations

  • Platforms evolve. Stripe added the setup_intent for saved payment methods; PayPal's Orders API is newer than the legacy Payments API.
  • Regulatory states change. EU PSD2 introduced strong customer authentication; states evolved to support it.
  • Refund + dispute timelines. Real-world chargebacks take weeks; test environments shortcut this.

References

  • Stripe PaymentIntent lifecycle: docs.stripe.com/payments/payment-intents (opens in new window).
  • Per-platform state machines (terminology grid, Stripe / Adyen / PayPal / Braintree state machines, webhook / refund / dispute detail, with their provider-doc citations): references/payment-state-machines.md.
  • Companion catalogs: 3ds-test-flow-reference, pci-dss-scope-reference.
  • Consumed by: stripe-test-cards-and-webhooks, adyen-test-mode, paypal-sandbox, braintree-test-cards, refund-test-matrix-builder, chargeback-flow-test-author, payment-webhook-replay.

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 stateStripeAdyenPayPalBraintree
Createdrequires_payment_methodReceivedCREATEDcreated
Awaiting action (3DS, etc.)requires_actionRedirectShopperPAYER_ACTION_REQUIREDn/a (handled inline)
ProcessingprocessingPendingPENDINGsubmitted_for_settlement
Authorized (not captured)requires_captureAuthorisedAPPROVED (no immediate capture)authorized
Captured / succeededsucceeded[Capture] SettledCOMPLETEDsettled
Failedfailed (charge)RefusedDECLINEDgateway_rejected / failed
CancelledcanceledCancelledVOIDEDvoided
Refundedsucceeded + refund object[Refund] SettledREFUNDEDrefunded
Disputed / chargebackdisputed (in dispute object)[Chargeback]disputedisputed

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.succeeded

Refund 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.

StateMeaning
InquiryBank requests info; not yet a chargeback
Pre-arbitrationInitial dispute filed
WonMerchant evidence accepted
LostMerchant evidence rejected; funds returned to customer
Pre-arbitration acceptedMerchant 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.