Test Cards & Sandbox Testing
Introduction
Sandbox mode lets you exercise card verification — success, 3DS challenge, declines, and the HIGHEST two-hold flow — deterministically 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:
| PAN | Brand | Default-path outcome |
|---|---|---|
4000 0000 0000 1117 | Visa | 3DS verification succeeds |
4242 4242 4242 4241 | Visa | Invalid PAN (Luhn check fails) |
5156 7637 1936 6465 | Mastercard | Invalid PAN (Luhn check fails) |
5555 5555 5555 4444 | Mastercard | ASI pre-check fails (rejected at create) |
Same card, two contexts
5555 5555 5555 4444is 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.
| PAN | Brand | Scenario | Expected end state | decline_code |
|---|---|---|---|---|
4242424242424242 | Visa | Success (frictionless / challenge-approved) | completed | — |
4000002760003184 | Visa | 3DS challenge required (no frictionless path) | stripe-3ds → challenge → completed | — |
4000008400001629 | Visa | 3DS authentication fails | failed | — |
4000000000000101 | Visa | CVC check fails | failed | incorrect_cvc |
4000000000000002 | Visa | Generic decline | failed | generic_decline |
4000000000000069 | Visa | Expired card | failed | expired_card |
4000000000009979 | Visa | Stolen card (hard decline) | failed | stolen_card |
4000000000009995 | Visa | Insufficient funds | failed | insufficient_funds |
4000000000009987 | Visa | Lost card (hard decline) | failed | lost_card |
4000000000000119 | Visa | Processing error (transient) | failed | processing_error |
4000000000000341 | Visa | HIGHEST: verification hold declines | failed at place-holds | (hold decline) |
5555555555554444 | Mastercard | Frictionless (issuer attests) | MEDIUM/HIGH → completed; HIGHEST → two-hold flow → completed | — |
5200828282828210 | Mastercard | 3DS challenge → success | fingerprint → challenge → completed | — |
2223003122003222 | Mastercard | 3DS challenge → authentication fails | failed (challenge POST → 500) | — |
5105105105105100 | Mastercard | 3DS challenge → abandoned | failed (challenge POST → 409) | — |
5555558265554449 | Mastercard | Consent 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.
-
Create the verification —
POST /card-verifications/3dswith a token scoped to the sandbox subaccount. CVC is required.{ "pan": "4242424242424242", "expiryMonth": 12, "expiryYear": 2034, "cvc": "123", "cardholderName": "Sandbox Test" } -
Branch on the response. When a challenge is required you get
currentStepId: "stripe-3ds"with aclientSecretand a test-modestripePublishableKey:{ "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 returnsstate: "completed"directly (nostripe-3dsstep). -
Complete the step —
POST /card-verifications/3ds/{verificationId}/steps/stripe-callbackafterhandleNextActionresolves, thenGET /card-verifications/3ds/{verificationId}to read the finalstate.
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
stripePublishableKeyis 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
- Quick Start: Card Enrollment — the end-to-end on-ramp.
- Verification Risk Tiers — what each tier does.
- Error States & Remediation — reproduce and handle every failure path with the matrix above.