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 code

The 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:

CardNetworkWhat happens
4242 4242 4242 4242VisaFrictionless: verification completes with no challenge
5555 5555 5555 4444MastercardFrictionless: verification completes with no challenge
4000 0027 6000 3184Visa3DS challenge, then success
5200 8282 8282 8210Mastercard3DS challenge, then success
4000 0084 0000 1629Visa3DS 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

getAccessToken must return a token minted server-side with your sandbox client credentials, exactly as in production. Minting from the browser fails with 403 {"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 fires onSuccess with { 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 and onSuccess fires with the same payload.
  • The failing card (4000 0084 0000 1629) fires onError with type: "server", the cardId and subscriptionId, and the failure detail. Look the verification up with those ids to read its failureReason; 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 SDK

Step 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:

  1. cardsubscription.created with state: "reqSCA" after the card subscription is created.
  2. cardsubscription.updated with state: "active" after the verification completes.
  3. transactionmessage.created for each message the scenario publishes, and transaction.created then transaction.updated as 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


Did this page help you?