The short version

Run each checkout scenario against your processor's sandbox using the test card numbers that processor publishes, then verify four things per scenario: the authorization result, the order state in your own database, the webhook your server receives, and what the buyer sees on screen. If all four agree, the scenario passes. When they disagree, the bug is usually in the webhook handler or the order state, not in the charge.

read more

One rule sits above the rest: no live card numbers in test. Not a colleague's, not one a customer emailed you, not a number you found in a shared document.

read more

Make the sandbox behave like production

A sandbox with different settings from production tells you almost nothing. Before you write a single test case, line up the environment:

read more

  • Same gateway account configuration, including capture mode (auth only versus auth and capture), AVS and CVV rules, and fraud filters.
  • Same 3-D Secure profile, so frictionless and challenge flows behave the way real issuers will trigger them.
  • Same currency, same amount limits, same tax and shipping logic.
  • A webhook endpoint that is publicly reachable and logs raw payloads.

The test matrix

Copy this list into your tracker and give every row a pass or fail. It covers the scenarios that break most often.

more on this topic

  1. Approval with no authentication.
  2. Approval with frictionless 3-D Secure.
  3. Challenge flow, authentication passed.
  4. Challenge flow, authentication failed.
  5. Challenge window closed or abandoned by the buyer.
  6. Decline: insufficient funds.
  7. Decline: expired card.
  8. Decline: incorrect security code.
  9. Decline: address mismatch, street only, ZIP only, and both.
  10. Decline: do not honor, and issuer unavailable.
  11. Timeout or dropped connection after the buyer submits.
  12. Double click on the pay button.
  13. Page refresh after a successful charge.
  14. Return from the 3-D Secure challenge on mobile, plus the back button.

What to assert, not just observe

A green checkmark on the gateway dashboard is not a passing test. Check these explicitly for every row above:

  • Exactly one order is created, and its status matches the authorization result.
  • Exactly one charge exists at the processor, with an idempotency key you can trace.
  • The webhook fires once, and a replayed copy does not create a second order or a second fulfillment job.
  • The confirmation page renders the right amount, currency, and last four digits.
  • A void and a partial refund both work against the same authorization.
  • The receipt email goes out once.

Where teams get burned

The failures I see most are boring and consistent. No idempotency key on the charge request, so a double submit becomes a double charge. The card form clears itself before the authorization result comes back, leaving the buyer with no idea what happened. The 3-D Secure return URL is relative and breaks behind a load balancer. The amount is hard-coded in the test harness, so currency rounding never gets exercised. Refunds get tested the day before launch, if at all.

The other common one is ignoring decline handling on the front end. A declined authorization should leave the cart intact and show a message the buyer can act on. If the cart empties on a decline, you will hear about it from support.

Keep test data clean

Use only the test numbers your processor publishes, and keep them in a fixture file, not scattered through the codebase. Test numbers are public and safe. Real numbers in a test environment put you in scope for PCI DSS in ways you do not want, and the standard is explicit that live data belongs nowhere near test systems. Never store a security code after authorization, in test or in production.

Sign-off

Log the gateway request ID, the response code, the timestamp, and the tester for every row. Re-run the full matrix after any change to gateway settings, 3-D Secure configuration, or the checkout code path. A checkout that passed last quarter is not a checkout that passes today.