Testing Single Card Enrollment
Introduction
This guide walks single-card enrollment end to end in the sandbox environment: create a subaccount, enroll a published test card with the Card Enrollment SDK, watch the 3DS verification complete, read the resources it created, then simulate a transaction on the card. Every request runs against https://api.sandbox.astrada.co with a bearer token from Get a token. The flow is identical to production; only the issuer behind the 3DS step is simulated, so each test card produces the same outcome every time.
Only published test cards can be enrolled here. The full list, with the outcome each card produces, is in Test Cards & Sandbox Testing.
Try it without writing codeThe hosted playground at sdk.sandbox.astrada.co/v1/playground.html is the SDK enrollment form on its own page. Enter your sandbox token URL and client credentials, pick the subaccount, and enroll any card from the test-card list. It runs the exact flow described below, so you can see the outcomes before writing a line of code.
1. Create a subaccount, set its verification tier, register a webhook
Create a fresh subaccount for the test run, so cleanup is trivial and events are easy to attribute:
POST /subaccounts HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"name": "card-enrollment-test",
"configurations": {
"VISA": { "countries": "*" },
"MASTERCARD": { "countries": "*" }
}
}curl -X POST https://api.sandbox.astrada.co/subaccounts \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "card-enrollment-test",
"configurations": {
"VISA": { "countries": "*" },
"MASTERCARD": { "countries": "*" }
}
}'{
"name": "card-enrollment-test",
"configurations": {
"VISA": { "countries": "*" },
"MASTERCARD": { "countries": "*" }
}
}Response (201 Created): the subaccount; keep its id as {subaccountId} for everything below.
Then set the subaccount's verification tier. stripeValidationLevel picks the risk tier the verification runs at (MEDIUM is the usual starting point; see Verification Risk Tiers). Test cards route to the simulated issuer automatically in the sandbox environment:
PATCH /subaccounts/{subaccountId} HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{ "verificationPolicy": { "stripeValidationLevel": "MEDIUM" } }curl -X PATCH https://api.sandbox.astrada.co/subaccounts/{subaccountId} \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "verificationPolicy": { "stripeValidationLevel": "MEDIUM" } }'{ "verificationPolicy": { "stripeValidationLevel": "MEDIUM" } }Response (200 OK): the subaccount, now carrying the tier.
Finally register a webhook for the enrollment and transaction events. A webhook.site URL works well as a throwaway receiver:
curl -X POST https://api.sandbox.astrada.co/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subaccountId": "{subaccountId}",
"url": "https://webhook.site/your-unique-url",
"description": "card enrollment test",
"eventTypes": [
"cardsubscription.created",
"cardsubscription.updated",
"transaction.created",
"transaction.updated",
"transactionmessage.created"
]
}'Response (201 Created): the webhook with its signing secret:
{
"id": "bcfa12e5-5870-415e-bef3-af8b3cbe159f",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"url": "https://webhook.site/your-unique-url",
"description": "card enrollment test",
"enabled": true,
"eventTypes": ["cardsubscription.created", "cardsubscription.updated", "transaction.created", "transaction.updated", "transactionmessage.created"],
"secret": "whsec_…",
"createdAt": "2026-09-02T01:44:46.481Z",
"updatedAt": "2026-09-02T01:44:46.481Z"
}2. Pick a test card
Each published test card fixes the verification outcome, so you choose the path you want to exercise by choosing the card. The cards used in this guide:
| Card | Network | What happens |
|---|---|---|
4242 4242 4242 4242 | Visa | Frictionless: verification completes with no challenge |
5555 5555 5555 4444 | Mastercard | Frictionless: verification completes with no challenge |
4000 0027 6000 3184 | Visa | 3DS challenge, then success |
5200 8282 8282 8210 | Mastercard | 3DS challenge, then success |
4000 0084 0000 1629 | Visa | 3DS authentication fails |
Any expiry in the future and any three-digit CVC work. The complete matrix, including decline and lockout cards and the HIGHEST two-hold flow, is in Test Cards & Sandbox Testing.
Submitting any other card number is rejected up front, before any processing, with a 403 and the error code card_subscription.sandbox_card_not_allowed. The allowlist matches the full card number exactly, never a prefix.
3. Enroll a card with the SDK
The Card Enrollment SDK renders the card form, the 3DS challenge and every follow-up step for you. In sandbox, load the bundle from the sandbox host; it detects the environment from its own origin and calls the sandbox API:
<script
type="text/javascript"
src="https://sdk.sandbox.astrada.co/v1/cardEnrollmentSdk.js"
data-id="card-enrollment-sdk"
></script>
<script>
CardEnrollmentSdk.openForm({
companyName: "Your company",
subaccountId: "{subaccountId}",
getAccessToken: () => fetch("/your-backend/astrada-sandbox-token").then((r) => r.text()),
onSuccess: (data) => console.log("enrolled", data),
onError: (data) => console.log("failed", data),
onCancel: () => console.log("cancelled"),
});
</script>
Mint the token on your backend
getAccessTokenmust return a token minted server-side with your sandbox client credentials, exactly as in production. Minting from the browser fails with403 {"error":"Invalid origin"}unless the calling origin is registered on your OAuth client.
Enter a card from step 2 with any future expiry and CVC, then submit:
- A frictionless card (
4242…,5555…) closes the form straight away and firesonSuccesswith{ subscriptionId, cardId, enrollmentGuidance, authenticationFlow }. - A challenge card (
4000 0027 6000 3184,5200 8282 8282 8210) shows the simulated issuer challenge inside the form; complete it andonSuccessfires with the same payload. - The failing card (
4000 0084 0000 1629) firesonErrorwithtype: "server", thecardIdandsubscriptionId, and the failure detail. Look the verification up with those ids to read itsfailureReason; every code is catalogued in Error States & Remediation.
The hosted playground linked at the top of this page runs this same call with your credentials, without any code.
4. Enroll a card with the API (optional)
Not needed when you use the SDKStep 3 already created the card subscription and ran the verification for you. This step shows the same sequence as raw API calls, for automated tests or an integration that renders its own card form. If you enrolled with the SDK, skip to step 5.
First create the card subscription:
POST /card-subscriptions HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"subaccountId": "{subaccountId}",
"pan": "4242424242424242",
"expiryMonth": 12,
"expiryYear": 2031,
"cvc": "123",
"cardholderName": "Sandbox Test",
"country": "USA"
}curl -X POST https://api.sandbox.astrada.co/card-subscriptions \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subaccountId": "{subaccountId}",
"pan": "4242424242424242",
"expiryMonth": 12,
"expiryYear": 2031,
"cvc": "123",
"cardholderName": "Sandbox Test",
"country": "USA"
}'{
"subaccountId": "{subaccountId}",
"pan": "4242424242424242",
"expiryMonth": 12,
"expiryYear": 2031,
"cvc": "123",
"cardholderName": "Sandbox Test",
"country": "USA"
}Response (201 Created): the subscription in reqSCA, waiting for the cardholder verification:
{
"id": "12c6fb2e-c7a9-4b54-9807-100a8e67b7a0",
"cardId": "639b3d2c-d365-4935-a63f-beed93334c1b",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"state": "reqSCA",
"enrollmentType": "cardholder-single",
"effectiveDate": "2026-09-02T01:28:29.278Z",
"expirationDate": "2031-12-31T23:59:59.999Z",
"createdAt": "2026-09-02T01:28:29.285Z",
"updatedAt": "2026-09-02T01:28:30.541Z"
}Then start the 3DS verification with the same card details:
POST /card-verifications/3ds HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"subaccountId": "{subaccountId}",
"pan": "4242424242424242",
"expiryMonth": 12,
"expiryYear": 2031,
"cvc": "123",
"cardholderName": "Sandbox Test"
}curl -X POST https://api.sandbox.astrada.co/card-verifications/3ds \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subaccountId": "{subaccountId}",
"pan": "4242424242424242",
"expiryMonth": 12,
"expiryYear": 2031,
"cvc": "123",
"cardholderName": "Sandbox Test"
}'{
"subaccountId": "{subaccountId}",
"pan": "4242424242424242",
"expiryMonth": 12,
"expiryYear": 2031,
"cvc": "123",
"cardholderName": "Sandbox Test"
}Response (201 Created) for a frictionless card: the verification is already completed, and the subscription activates a moment later:
{
"id": "69899a27-b819-4b28-aee0-73d2c9f3d636",
"type": "3DS",
"state": "completed",
"currentStepId": null,
"cardId": "639b3d2c-d365-4935-a63f-beed93334c1b",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"createdAt": "2026-09-02T01:28:38.036Z",
"updatedAt": "2026-09-02T01:28:38.036Z"
}For a challenge card the verification stays in-progress and hands you the challenge step, with a test-mode publishable key:
{
"id": "3319eb0e-68f9-4f6b-98c0-70bfdbe44712",
"type": "3DS",
"state": "in-progress",
"currentStepId": "stripe-3ds",
"cardId": "28a16fa8-b1d6-4716-82bb-9f7006d269cb",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"clientSecret": "seti_…_secret_…",
"stripePublishableKey": "pk_test_…"
}Completing that step needs a browser: the call sequence on the test-cards page shows how to run the challenge and post steps/stripe-callback, after which GET /card-verifications/3ds/{verificationId} returns the final state. The failing card takes the same branch and ends failed, with its failureReason on the GET.
5. See what was created
Read the subscription back; once the verification completed it is active:
curl https://api.sandbox.astrada.co/card-subscriptions/{subscriptionId} \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response (200 OK):
{
"id": "12c6fb2e-c7a9-4b54-9807-100a8e67b7a0",
"cardId": "639b3d2c-d365-4935-a63f-beed93334c1b",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"state": "active",
"enrollmentType": "cardholder-single",
"effectiveDate": "2026-09-02T01:28:29.278Z",
"expirationDate": "2031-12-31T23:59:59.999Z",
"createdAt": "2026-09-02T01:28:29.285Z",
"updatedAt": "2026-09-02T01:28:40.532Z"
}The card itself is at GET /cards/{cardId}:
{
"id": "639b3d2c-d365-4935-a63f-beed93334c1b",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"network": "visa",
"first6digits": "424242",
"last4digits": "4242",
"expiryMonth": 12,
"expiryYear": 2031,
"country": "USA",
"createdAt": "2026-09-02T00:37:34.917Z",
"updatedAt": "2026-09-02T00:37:34.917Z"
}Your webhook received cardsubscription.created with state: "reqSCA" when the subscription was created, then cardsubscription.updated with state: "active" when the verification completed. The SDK path produces exactly the same two events.
6. Simulate a transaction
With the card active, run a transaction scenario against it. amount is in minor units (1500 is 15.00) and currency is an ISO 4217 code; both are optional and default per scenario. For a Mastercard card pass "network": "MASTERCARD", otherwise the scenario's default network is used:
POST /simulate/transaction/scenario HTTP/1.1
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"scenario": "simple_purchase",
"cardId": "{cardId}",
"subaccountId": "{subaccountId}",
"amount": 1500,
"currency": "USD"
}curl -X POST https://api.sandbox.astrada.co/simulate/transaction/scenario \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scenario": "simple_purchase",
"cardId": "{cardId}",
"subaccountId": "{subaccountId}",
"amount": 1500,
"currency": "USD"
}'{
"scenario": "simple_purchase",
"cardId": "{cardId}",
"subaccountId": "{subaccountId}",
"amount": 1500,
"currency": "USD"
}Response (202 Accepted): one entry per transaction the scenario creates, each listing the message ids it will publish (an authorization and a clearing for simple_purchase):
{
"transactions": [
{ "messageIds": ["6863b28c-aeb3-4331-9663-9fc2ccfa5aed", "11d038b7-fe95-4ef6-9069-34aabe1e1d83"] }
]
}Processing is asynchronous. Within about thirty seconds the transaction is readable, and transaction.created, transaction.updated and transactionmessage.created arrive on the webhook:
curl "https://api.sandbox.astrada.co/transactions?cardId={cardId}" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response (200 OK):
{
"_embedded": {
"transactions": [
{
"id": "064861a4-bd59-43f4-b779-426357764268",
"cardId": "639b3d2c-d365-4935-a63f-beed93334c1b",
"subaccountId": "2c5a170e-bcdc-461a-880c-db962d610653",
"network": "VISA",
"status": "SETTLED",
"transactionType": "DEBIT_01",
"descriptor": "BLUE BOTTLE COFFEE",
"acceptor": { "city": "SAN FRANCISCO", "state": "CA", "country": "840", "mcc": "5814" },
"transactionCurrency": "USD",
"transactionHoldAmount": 0,
"transactionSettledAmount": 15,
"cardholderBillingCurrency": "USD",
"cardholderBillingHoldAmount": 0,
"cardholderBillingSettledAmount": 15,
"transactionOccurrenceDate": "2026-09-02",
"createdAt": "2026-09-02T01:36:19.057Z",
"updatedAt": "2026-09-02T01:36:20.366Z"
}
]
}
}The other scenarios (refund, hotel_preauth, partial_clearing, recurring_subscription, and more) exercise holds, adjustments and multi-message flows; the catalog and every parameter are in the Sandbox API Reference.
7. Webhooks checklist
Across a successful run your receiver sees, in order:
cardsubscription.createdwithstate: "reqSCA"after the card subscription is created.cardsubscription.updatedwithstate: "active"after the verification completes.transactionmessage.createdfor each message the scenario publishes, andtransaction.createdthentransaction.updatedas the transaction settles.
A failed verification leaves the subscription in reqSCA and produces no cardsubscription.updated.
8. Clean up
curl -X DELETE "https://api.sandbox.astrada.co/card-subscriptions/{subscriptionId}" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response (204 No Content): the subscription and, when it is the last subscription on the card, the card, cardholder and transactions are deleted. Deleting the subaccount itself (DELETE /subaccounts/{subaccountId}) tears down everything in one call; see Cleaning up test data.
Next steps
- Quick Start: Card Enrollment: the production on-ramp for the SDK.
- Test Cards & Sandbox Testing: every test card and the outcome it produces.
- Error States & Remediation: reproduce and handle every failure path.
- Sandbox API Reference: the simulation endpoint reference.
Updated about 1 hour ago
