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:

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:

PAN Brand Default-path outcome
4000 0000 0000 1117 Visa 3DS is bypassed (test-card exemption) and 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 4444 is 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:

HTTPcURLRequest body

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 none
4000002760003184 Visa Consumer 3DS challenge required (no frictionless path) stripe-3ds β†’ challenge β†’ completed none
4000008400001629 Visa Consumer 3DS authentication fails failed none
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 none
5555555555554444 Mastercard Consumer Frictionless (issuer attests) MEDIUM/HIGH β†’ completed; HIGHEST β†’ two-hold flow β†’ completed none
5200828282828210 Mastercard Consumer 3DS challenge β†’ success fingerprint β†’ challenge β†’ completed none
2223003122003222 Mastercard Consumer 3DS challenge β†’ authentication fails failed (challenge POST β†’ 500) none
5105105105105100 Mastercard Consumer 3DS challenge β†’ abandoned failed (challenge POST β†’ 409) none
5555558265554449 Mastercard Consumer Network pre-check declined (rejected at create) 400 at create, no verification created none
5200828282828897 Mastercard Commercial Frictionless (issuer attests) MEDIUM/HIGH β†’ completed; HIGHEST β†’ two-hold flow β†’ completed none
5555552500001001 Mastercard Commercial 3DS challenge β†’ success fingerprint β†’ challenge β†’ completed none

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:

JSON

{
  "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.

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

JSON

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

JSON

{
     "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).

  1. Complete the step: POST /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:

JSON

{ "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

Next steps