Test Cards & Sandbox Testing

Introduction

Sandbox mode lets you exercise card verification — success, 3DS challenge, declines, and the HIGHEST two-hold flowdeterministically and with no real money movement, against the production API. You enable it per subaccount, then enroll known test cards.

How it works

On a sandbox-enabled subaccount, the verification service routes by card number:

  • A known test card (the matrix below) runs against the Stripe sandbox (test mode): you get real Stripe 3DS / decline behavior, deterministically.
  • Any other card runs against live processing, exactly as in production.

Routing is fail-closed: it requires the subaccount to be explicitly sandbox-enabled, and it matches the full card number exactly (never a BIN or prefix). On a normal production subaccount, a test card is not diverted — it goes live and is declined as invalid. A configuration gap degrades to real validation, never to a fake success.

Sandbox applies to every tier, including HIGHEST: the two-hold flow runs as test-mode authorizations, so you can drive it end to end.

Default path vs sandbox

How a card is verified depends on whether the subaccount is sandbox-enabled:

  • Sandbox-enabled (verificationPolicy.sandbox: true) — known test cards route to the Stripe sandbox (Visa) and the TNS emulator (Mastercard) for deterministic outcomes (the sandbox matrix below). Recommended for testing.
  • Default (non-sandbox) — cards hit the real networks (TokenEx/IXOPAY for Visa, the Mastercard Consents/ASI flow for Mastercard). A few network test cards exist for basic enrollment checks:
PANBrandDefault-path outcome
4000 0000 0000 1117Visa3DS verification succeeds
4242 4242 4242 4241VisaInvalid PAN (Luhn check fails)
5156 7637 1936 6465MastercardInvalid PAN (Luhn check fails)
5555 5555 5555 4444MastercardASI pre-check fails (rejected at create)
📘

Same card, two contexts

5555 5555 5555 4444 is rejected at create (ASI pre-check failure) on the default path, but on a sandbox-enabled subaccount the TNS emulator returns a frictionless success for it (see the sandbox matrix below). Test against a sandbox subaccount for deterministic behavior.

Enabling sandbox mode

Set sandbox on the subaccount's verificationPolicy via Update Subaccount:

PATCH /subaccounts/{subaccountId}
Authorization: Bearer <token with subaccounts:write>

{
  "verificationPolicy": { "sandbox": true }
}

Use a dedicated sandbox subaccount, never a production one — coordinate with your Astrada contact to provision one. Anything other than true (absent, false, null) means live processing. You still call the production API with a token scoped to the sandbox subaccount.

Sandbox test-card matrix

These are the deterministic outcomes on a sandbox-enabled subaccount. All cards use any future expiry (e.g. 12/2034) and any 3-digit CVC (123) unless the row is about CVC. Outcomes are verified against the real Stripe sandbox / TNS emulator.

PANBrandScenarioExpected end statedecline_code
4242424242424242VisaSuccess (frictionless / challenge-approved)completed
4000002760003184Visa3DS challenge required (no frictionless path)stripe-3ds → challenge → completed
4000008400001629Visa3DS authentication failsfailed
4000000000000101VisaCVC check failsfailedincorrect_cvc
4000000000000002VisaGeneric declinefailedgeneric_decline
4000000000000069VisaExpired cardfailedexpired_card
4000000000009979VisaStolen card (hard decline)failedstolen_card
4000000000009995VisaInsufficient fundsfailedinsufficient_funds
4000000000009987VisaLost card (hard decline)failedlost_card
4000000000000119VisaProcessing error (transient)failedprocessing_error
4000000000000341VisaHIGHEST: verification hold declinesfailed at place-holds(hold decline)
5555555555554444MastercardFrictionless (issuer attests)MEDIUM/HIGH → completed; HIGHEST → two-hold flow → completed
5200828282828210Mastercard3DS challenge → successfingerprintchallengecompleted
2223003122003222Mastercard3DS challenge → authentication failsfailed (challenge POST → 500)
5105105105105100Mastercard3DS challenge → abandonedfailed (challenge POST → 409)
5555558265554449MastercardConsent declined (rejected at create)400 at create — no verification created

Under MEDIUM and HIGH the issuer is asked to challenge, so most Visa cards first return currentStepId: "stripe-3ds" and require the 3DS step before reaching the final state. Stolen / lost / fraudulent cards hard-decline at every tier — no tier lets them through.

Mastercard sandbox. Mastercard test cards are served by an in-process emulator — the live Mastercard network is never called. The 3DS challenge is simulated: there is no real issuer challenge screen to render; advance the step by calling POST /steps/challenge and the outcome is fixed by the test card. At HIGHEST, a frictionless Mastercard card (5555…) takes the two-hold path; a passed challenge completes with no holds.

Call sequence

Point the SDK at the sandbox subaccount and it drives this whole sequence for you. The raw steps below are for direct/automated API testing.

  1. Create the verificationPOST /card-verifications/3ds with a token scoped to the sandbox subaccount. CVC is required.

    { "pan": "4242424242424242", "expiryMonth": 12, "expiryYear": 2034, "cvc": "123", "cardholderName": "Sandbox Test" }
  2. Branch on the response. When a challenge is required you get currentStepId: "stripe-3ds" with a clientSecret and a test-mode stripePublishableKey:

    {
      "id": "…", "currentStepId": "stripe-3ds", "state": "in-progress",
      "clientSecret": "seti_…_secret_…", "stripePublishableKey": "pk_test_…"
    }

    Load Stripe.js with that publishable key and call stripe.handleNextAction({ clientSecret }). A frictionless success returns state: "completed" directly (no stripe-3ds step).

  3. Complete the stepPOST /card-verifications/3ds/{verificationId}/steps/stripe-callback after handleNextAction resolves, then GET /card-verifications/3ds/{verificationId} to read the final state.

Testing the HIGHEST two-hold flow

A sandbox test card has no banking app to read the hold amounts from, so on a sandbox verification the place-holds response includes the amounts:

{ "currentStepId": "stripe-amount-confirm", "sandboxHoldAmounts": { "amount1": 57, "amount2": 88 } }

sandboxHoldAmounts is test-mode only — never present on a live verification, and never on GET. Feed the two values straight into POST /steps/amount-confirm to drive the success path. See HIGHEST Verification for the full flow.

Notes

  • This routes real test cards through Stripe's sandbox on the production endpoint — only on a subaccount flagged for sandbox use. Always use a separate sandbox subaccount.
  • The only behavioral difference on a sandbox verification is that the returned stripePublishableKey is the test-mode key, so Stripe.js drives the challenge against the test account. Endpoints and request/response shapes are otherwise identical to production.
  • The verification response includes authenticationFlow (frictionless | challenge) so you can distinguish a frictionless approval from a completed challenge.

Next steps