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 verification sandbox (test mode): you
get real 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
verification sandbox (for both Visa and Mastercard) for deterministic outcomes (the
sandbox matrix below). Recommended for testing. - Default (non-sandbox) — cards hit the real networks (Visa and Mastercard), exactly as in
production. A few network test cards exist for basic enrollment checks:
| PAN | Brand | Default-path outcome |
|---|---|---|
4000 0000 0000 1117 | Visa | 3DS is bypassed (test-card exemption) — resolves via authenticationFlow: null, not a challenge/frictionless pass |
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 | Network pre-check fails (rejected at create) |
Same card, two contexts
5555 5555 5555 4444is rejected at create (network pre-check failure) on the default path, but on
a sandbox-enabled subaccount the sandbox 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} HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"verificationPolicy": { "sandbox": true }
}curl -X PATCH https://api.astrada.co/subaccounts/{subaccountId} \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"verificationPolicy": { "sandbox": true }
}'{
"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 verification sandbox.
The Product column is the card type the sandbox reports to the enrollment card-type control (blockedCardTypes): a subaccount that blocks CONSUMER cards rejects every consumer PAN below and accepts a COMMERCIAL one — so you can test a consumer-card block end to end.
| PAN | Brand | Product | Scenario | Expected end state | failureReason.code |
|---|---|---|---|---|---|
4242424242424242 | Visa | Consumer | Success (frictionless / challenge-approved) | completed | — |
4000002760003184 | Visa | Consumer | 3DS challenge required (no frictionless path) | stripe-3ds → challenge → completed | — |
4000008400001629 | Visa | Consumer | 3DS authentication fails | failed | — |
4000000000000101 | Visa | Consumer | CVC check fails | failed | incorrect_cvc |
4000000000000002 | Visa | Consumer | Generic decline | failed | generic_decline |
4000000000000069 | Visa | Consumer | Expired card | failed | expired_card |
4000000000009979 | Visa | Consumer | Stolen card (hard decline) | failed | stolen_card |
4000000000009995 | Visa | Consumer | Insufficient funds | failed | insufficient_funds |
4000000000009987 | Visa | Consumer | Lost card (hard decline) | failed | lost_card |
4000000000000119 | Visa | Consumer | Processing error (transient) | failed | processing_error |
4000000000000341 | Visa | Consumer | HIGHEST: verification hold declines | failed at place-holds | (hold decline) |
4000000000003220 | Visa | Commercial | 3DS challenge → success | stripe-3ds → challenge → completed | — |
5555555555554444 | Mastercard | Consumer | Frictionless (issuer attests) | MEDIUM/HIGH → completed; HIGHEST → two-hold flow → completed | — |
5200828282828210 | Mastercard | Consumer | 3DS challenge → success | fingerprint → challenge → completed | — |
2223003122003222 | Mastercard | Consumer | 3DS challenge → authentication fails | failed (challenge POST → 500) | — |
5105105105105100 | Mastercard | Consumer | 3DS challenge → abandoned | failed (challenge POST → 409) | — |
5555558265554449 | Mastercard | Consumer | Network pre-check declined (rejected at create) | 400 at create — no verification created | — |
5200828282828897 | Mastercard | Commercial | Frictionless (issuer attests) | MEDIUM/HIGH → completed; HIGHEST → two-hold flow → completed | — |
5555552500001001 | Mastercard | Commercial | 3DS challenge → success | fingerprint → challenge → completed | — |
failureReason.code is an overloaded field — for Stripe-routed failures like these it's the raw Stripe decline code shown above; for network-3DS failures elsewhere it instead carries a 3DS transStatusReason code. Check failureReason.category first if you're branching on failure type generically.
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.
Only test cards enroll in the sandbox environment
In the dedicated sandbox environment, card enrollment accepts only the published test cards in the matrix above. Submitting any other PAN to POST /card-subscriptions is rejected up front, before any network call, with a typed 403:
{
"title": "Card cannot be enrolled",
"detail": "This card is not a sandbox test card. In the sandbox environment, only published Astrada test cards can be enrolled. Use a card from the sandbox test-card list: https://docs.astrada.co/docs/sandbox-testing",
"errorCode": "card_subscription.sandbox_card_not_allowed",
"type": "https://docs.astrada.co/reference/errors-sandbox_card_not_allowed"
}This keeps real PANs out of the non-PCI sandbox and turns what used to be a silent downstream failure into a clear, actionable error at the door. The allowlist is matched on the full card number exactly (never a BIN or prefix). This is distinct from the per-subaccount verificationPolicy.sandbox routing above: that controls how a card is verified on the production API; this controls which cards may be enrolled at all in the dedicated sandbox environment — see Sandbox for that environment's other endpoints (transaction simulation, network bulk-feed ingestion).
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", "subaccountId": "<SUBACCOUNT_ID>" } -
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-callback
afterhandleNextActionresolves, 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 the verification 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 the SDK drives the challenge against the test
environment. Endpoints and request/response shapes are otherwise identical to production. - The verification response includes
authenticationFlow(frictionless|challenge|nullif
3DS didn't run) 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.