Single Card Enrollment — API Reference

Introduction

Customers with personal cards or business cards can add their cards individually. Cardholders are required to complete a verification process to ensure they are the owner of the card when they link it.

Enrollment is done with Astrada's pre-built Card Enrollment SDK — see Get the SDK and Installation. This page is the API reference the SDK implements; the endpoint-level detail is for understanding the flow (and for approved partners with direct-API access).

This page walks you through the process and steps that cardholders will take when linking cards through a Single Card Enrollment experience.

❗️

You do not make these API calls yourself

The SDK performs every call on this page for you. A standard integration is just: embed the SDK → handle onSuccess / onError. The walkthrough below documents what the SDK does under the hood — read it to understand the flow; only approved partners with direct-API access implement it themselves.

The enrollment flow (under the hood)

1. Customer Introduction

The first stage we recommend in any card enrollment journey is to clearly outline to your customers the scope of the data sharing they are about to consent to and any important information about how their data will be used.

This step improves user conversion by providing a feeling of trust and security.

2. Card Data Collection

The second stage of the card linking journey requires the collection of card data and consent. It is at this stage that a user provides you with their sensitive data and opts into the terms of the data sharing arrangement explicitly.

API Request

POST https://api.astrada.co/card-subscriptions
{
    "subaccountId": "<SUBACCOUNT_ID>",
    "country": "<ISSUING COUNTRY>",
    "expiryMonth": "<EXP_MONTH>",
    "expiryYear": "<EXP_YEAR>",
    "pan": "<CARD_NUMBER>",
    "cvc": "<CVC_NUMBER>",
    "cardholderName": "<HOLDER NAME>"
}

This call starts the card enrollment process onto a specific Subaccount — the SDK makes it when the cardholder submits the card form. A successful create returns 201 Created.

409 Conflict — subscription already exists. If a subscription for the same card already exists on this subaccount, POST /card-subscriptions returns 409 with detail: "There is already a card subscription for the specified card". Treat this as a non-fatal "already enrolled" signal — the SDK does this by issuing a GET and continuing with the existing subscription's state.

3. Start card verification

Card verification steps:

As mentioned here, this is the expected 3DS card verification flow:

  1. Device fingerprint is collected;
  2. Device fingerprint is transmitted to the issuer's Access Control System (ACS)
  3. Depending on issuer response a (3DS) challenge is required
    1. If the fingerprint is determined to be low risk, the challenge step is skipped. (Risk Assessment factors here)
    2. If the fingerprint is approved but higher risk, the challenge is initiated
    3. If the fingerprint fails, it is usually due to a lost/stolen card, an inactive card, or a card that is otherwise unable to transact.

Every card subscription created through Single Card Enrollment is subject to cardholder verification, so the SDK immediately follows up with the call to start it:

📘

Choosing how strict verification is

New to enrollment? Start with Quick Start: Card Enrollment for the end-to-end flow. The verification experience above depends on the subaccount's risk tier. See Verification Risk Tiers to pick a tier, HIGHEST Verification for the strictest two-hold flow, Sandbox Testing & Test Cards to test each tier, and Error States & Remediation for every failure path.

API Request

POST https://api.astrada.co/card-verifications/3ds
{
    "subaccountId": "<SUBACCOUNT_ID>",
    "country": "<ISSUING COUNTRY>",
    "expiryMonth": "<EXP_MONTH>",
    "expiryYear": "<EXP_YEAR>",
    "pan": "<CARD_NUMBER>",
    "cvc": "<CVC_NUMBER>",
    "cardholderName": "<HOLDER NAME>"
}

A successful call returns a Card Verification ID (id), used throughout the subsequent steps to track the current card verification step. A successful create returns 201 Created.

Starting card verification on a previously created subscription uses this same endpoint.

If at any given point in time you want to check the card verification status on a particular subscription, you can either make the GET Card Verification request, or a List a Card Verification steps (below)

GET https://api.astrada.co/card-verifications/3ds/{verificationId}
GET https://api.astrada.co/card-verifications/3ds/{verificationId}/steps

In most successful cases, the response for this request will direct the card holder verification to perform device fingerprint. This should be evident through the currentStep property in the response, with a link to the fingerprint step. If this is the case, follow steps outlined on step 3.1 below.

{
  "id": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "cardId": "8309b5f8-d5d8-49bb-9001-38bf1bb0f1e4",
  "type": "3DS",
  "currentStepId": "fingerprint",
  "state": "in-progress",
  "createdAt": "2024-01-04T18:53:32.000Z",
  "updatedAt": "2023-01-04T10:55:12.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b"
    },
    "steps": {
      "href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps"
    },
    "currentStep": {
      "href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/fingerprint"
    }
  }
}

3.1 Device Fingerprint

As mentioned here, the card verification flow follows a 1 or 2 step process, depending on the risk appreciation of the 3DS provider (Issuer).

The first step is to attempt a frictionless verification flow by collecting device and browser data to infer identity verification, known as Device Fingerprinting. The SDK triggers this with the following API call:

GET https://api.astrada.co/card-verifications/3ds/{verificationId}/steps/fingerprint

Use the Card Verification ID (id) mentioned on step 3 above

{
  "id": "fingerprint",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "verificationId": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "type": "fingerprint",
  "state": "in-progress",
  "data": {
    "threeDSMethodData": "eyJ0aHJlZURTTWV0aG9kTm90aWZpY2F0aW9uVVJMIjoiaHR0cHM6Ly9zYW5kYm94LmFwaS5tYXN0ZXJjYXJkLmNvbS9vcGVuYXBpcy9hdXRoZW50aWNhdGlvbi9jYWxsYmFja3MvdGhyZWVEU01ldGhvZE5vdGlmaWNhdGlvbiIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiOTNhN2NjNzUtY2I3Yy00Y2QzLWEwNTMtYjJjNGMxODZiZTVmIn0=",
    "threeDsMethodUrl": "https://acs-public.tp.mastercard.com/api/v1/3ds_method",
    "threeDSMethodNotificationURL": "https://sandbox.api.mastercard.com/openapis/authentication/callbacks/threeDSMethodNotification",
    "threeDSServerTransID": "93a7cc75-cb7c-4cd3-a053-b2c4c186be5f"
  },
  "createdAt": "2024-01-05T14:46:50.000Z",
  "updatedAt": "2024-01-05T14:46:51.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/fingerprint"
    },
    "nextStep": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/challenge"
    }
  }
}

The SDK uses the data properties to open a hidden iframe and collect the user's browser information. This hidden iframe POSTS to ACS (Access Control System) and the response is a page that collects the device information and sends it to ACS. The fingerprint iframe posts a message to the window (threeDSMethodNotificationURL) when it is complete.

Under the hood it runs HTML like the snippet below:

<html>
    <head>
        <script src="/static/fingerprint.js"></script>
        <script>
            window.onload = function() {
                doFingerprint(
                    '{{ threeDsMethodUrl }}',
                    '{{ threeDSMethodNotificationURL }}',
                    '{{ threeDSMethodData }}',
                    '{{ threeDSServerTransID }}');
            }
        </script>
    </head>
</html>

On load calls the doFingerprint javascript function as such:

// This listener receives events from the window.
// On the arrival of threeds-method-notification event, it forwards 
// the required data to the server to start authentication.
function fingerprintCompleteListener(m) {
    if (m.data.type === 'threeds-method-notification') {
        console.log('fingerprintCompleteListener called');
        proceedAfterFingerprint('complete');
    }
};

// Next step after fingerprinting (either when it is completed or it was not needed)
function proceedAfterFingerprint(fingerprintStatus) {
    // Below mentioned parameters controls the looks of ACS challenge window. Values differs as per device configuration.
    // For simplicity we have hardcoded some of them.
    const body = {
        fingerprintStatus: fingerprintStatus,
        browserData: {
          challengeWindowSize: '04', // 600x400
        	acceptHeader: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
       		colorDepth: window.screen.colorDepth,
        	javaEnabled: true,
        	language: navigator.language,
        	screenHeight: window.screen.height,
        	screenWidth: window.screen.width,
        	timezone: new Date().getTimezoneOffset(),
          userAgent: window.navigator.userAgent,
        },
    };
    post("https://api.astrada.co/card-verifications/3ds/{verification-id}/steps/fingerprint", body);
};

function doFingerprint(threeDsMethodUrl, threeDSMethodNotificationURL, threeDSMethodData, threeDSServerTransID) {
    if (threeDsMethodUrl) {
        const html = `<script>
                document.addEventListener("DOMContentLoaded", function () {
                    var form = document.createElement("form");
                    form.method = "POST";
                    form.action = "${threeDsMethodUrl}";
                    form.appendChild(createInput("threeDSMethodNotificationURL", "${threeDSMethodNotificationURL}"));
                    form.appendChild(createInput("threeDSMethodData", "${threeDSMethodData}"));
                    form.appendChild(createInput("threeDSServerTransID", "${threeDSServerTransID}"));
                    document.body.appendChild(form);
                    form.submit();
                    document.body.removeChild(form);
                });
                function createInput(name, value) {
                    var result = document.createElement("input");
                    result.name = name;
                    result.value = value;
                    return result;
                }
            </script>`

        const iframe = document.createElement("iframe");
        iframe.id = '3ds-fingerprint';
        document.body.appendChild(iframe);
        iframe.style.display = "none";
        const win = iframe.contentWindow;
        if (win != null) {
            const doc = win.document;
            win.name = "3DS Fingerprint";
            doc.open();
            doc.write(html);
            doc.close();
        }
        window.addEventListener("message", fingerprintCompleteListener);
    } else {
        // No threeDsMethodUrl so skip fingerprinting
        proceedAfterFingerprint('unavailable');
    }
};

Once fingerprinting is complete, the browser details are sent to the ACS so it can correctly size the challenge iframe that may be prompted next.

The fingerprintStatus in the request body is the client-side status of the fingerprint iframe — one of complete, timeout, or unavailable. The outcome in the response is a separate field the service returns to indicate what happens next; do not conflate the two.

That happens via the following request:

POST https://api.astrada.co/card-verifications/3ds/{verificationId}/steps/fingerprint
{
   "fingerprintStatus":"complete",
   "browserData":{
      "colorDepth":24,
      "language":"en-US",
      "timezone":-120,
      "screenHeight":1080,
      "screenWidth":1920,
      "challengeWindowSize":"05",
      "javaEnabled":false,
      "acceptHeader":"application/json",
      "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
      "ip":"123.0.0.0"
   }
}

No further action — outcome: authenticated

If the provider and issuer find this information sufficient, the verification completes frictionlessly — no further step:

{
  "id": "fingerprint",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "verificationId": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "type": "fingerprint",
  "state": "completed",
  "outcome": "authenticated",
  "createdAt": "2024-01-05T14:46:50.000Z",
  "updatedAt": "2024-01-05T14:46:51.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/fingerprint"
    },
    "nextStep": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/challenge"
    }
  }
}

Challenge is required — outcome: requires-challenge

If the issuer still needs the cardholder to complete a challenge, the response says so:

If this is the case, follow the steps outlined in 3.2.

{
  "id": "fingerprint",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "verificationId": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "type": "fingerprint",
  "state": "completed",
  "outcome": "requires-challenge",
  "createdAt": "2024-01-05T14:46:50.000Z",
  "updatedAt": "2024-01-05T14:46:51.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/fingerprint"
    },
    "nextStep": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/challenge"
    }
  }
}

Verification failed — outcome: failed

A terminal state: the verification ends as failed with no further step to call. Common causes: a lost/stolen card, an inactive card, or a card otherwise unable to transact.

{
  "id": "fingerprint",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "verificationId": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "type": "fingerprint",
  "state": "completed",
  "outcome": "failed",
  "createdAt": "2024-01-05T14:46:50.000Z",
  "updatedAt": "2024-01-05T14:46:51.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/fingerprint"
    }
  }
}

3.2 3DS Challenge

In some instances of single card enrollment where device fingerprinting information needs to be complemented with more data, the cardholder will need to provide this additional verification data to their issuer through the embedded iframe portal. The card holder will be prompted with a token-based challenge, which they will need to provide through this iframe.

When the token-based challenge is executed, it will go to the registered cardholder, generally in the form of a push notification on the issuer app, or text SMS.

The iframe is completely controlled by the card verification provider, and therefore it is expected that you use the URL that is provided by them (example below).

The URL for the challenge iframe comes from the Get Card Verification step challenge data endpoint — the acsUrl property nested under data.

GET https://api.astrada.co/card-verifications/3ds/{verificationId}/steps/challenge
{
  "id": "challenge",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "verificationId": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "type": "challenge",
  "state": "in-progress",
  "data": {
    "acsUrl": "https://acs-public.tp.mastercard.com/api/v1/browser_challenges",
    "encodedCReq": "eyJ0aHJlZURTU2VydmVyVHJhbnNJRCI6IjkzYTdjYzc1LWNiN2MtNGNkMy1hMDUzLWIyYzRjMTg2YmU1ZiIsImFjc1RyYW5zSUQiOiJiODBkNTZkNy01N2I1LTRhMzAtYmYwZC0xNzE4ZDlmNzI1ZTYiLCJjaGFsbGVuZ2VXaW5kb3dTaXplIjoiMDQiLCJtZXNzYWdlVHlwZSI6IkNSZXEiLCJtZXNzYWdlVmVyc2lvbiI6IjIuMi4wIn0"
  },
  "createdAt": "2024-01-05T14:46:51.000Z",
  "updatedAt": "2024-01-05T18:47:22.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/challenge"
    }
  }
}

The SDK displays the 3DS challenge iframe with HTML like the snippet below.

<html>
    <head>
        <script src="/static/challenge.js"></script>
        <script>
            window.onload = function() {
                doChallenge('{{ acsUrl }}', '{{ encodedCReq }}');
            }
        </script>
    </head>
    <body>
        Performing 3DS challenge.
    </body>
</html>

On page load it calls doChallenge javascript function:

// This listener receives messages posted to the window. After listening 
// threeds-challenge-notification message, the challenge results window will pop-up.
function challengeCompleteListener(m) {
    if (m.data.type === 'threeds-challenge-notification') {
        console.log("challengeCompleteListener called");
				post("https://api.astrada.co/card-verifications/3ds/{verification-id}/steps/challenge", {});    
    }
};

// Opens 3DS challenge iframe and listens to event completion.
function doChallenge(acsUrl, encodedCReq) {

    const html = `<script>
            document.addEventListener("DOMContentLoaded", function () {
                var form = document.createElement("form");
                form.method = "POST";
                form.action = "${acsUrl}";
                form.appendChild(createInput("creq", "${encodedCReq}"));
                document.body.appendChild(form);
                form.submit();
                document.body.removeChild(form);
            });
            function createInput(name, value) {
                var result = document.createElement("input");
                result.name = name;
                result.value = value;
                return result;
            }
        </script>`

    const iframe = document.createElement("iframe");
    iframe.id = "3ds-challenge";
    iframe.width = "600px";
    iframe.height = "400px";
    iframe.frameBorder = "0";
    iframe.style.display = 'block';
    iframe.style.position = 'absolute';
    iframe.style.top = "100px";
    iframe.style.left = "50%";
    iframe.style.transform = "translate(-50%, 0%)";
    iframe.style.background = "white";
    document.body.appendChild(iframe);
    const win = iframe.contentWindow;

    if (win != null) {
        const doc = win.document;
        win.name = "3DS Challenge";
        doc.open();
        doc.write(html);
        doc.close();
    }

    window.addEventListener("message", challengeCompleteListener);
};

Once the challenge iframe is complete it posts a message to the window (threeDSMethodNotificationURL) to get the results of the challenge (step 5 below).

4. Success & Completion

Once the cardholder has successfully provided the challenge data to their issuer, a success state is returned.

409 Conflict on the challenge POST. POST /card-verifications/3ds/{verificationId}/steps/challenge returns 409 if the verification has been abandoned, or if the challenge results are not yet ready when the call is made. Retry shortly, or refresh the verification state via GET /card-verifications/3ds/{verificationId}.

The SDK completes the verification with the following request when the challenge posts back, then checks state for completed or failed.

POST https://api.astrada.co/card-verifications/3ds/{verificationId}/steps/challenge
{
  "id": "challenge",
  "subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
  "verificationId": "9fab1bea-bc1e-4757-bd47-479422e5983b",
  "type": "challenge",
  "state": "completed",
  "createdAt": "2024-01-05T14:46:51.000Z",
  "updatedAt": "2024-01-05T18:47:22.000Z",
  "_links": {
    "self": {
      "href": "/card-verifications/9fab1bea-bc1e-4757-bd47-479422e5983b/3ds/steps/challenge"
    }
  }
}

Errors

Card Enrollment

ValidationHTTPMessage detail (API)Message detail (SDK)
Incorrect subaccount Id400Invalid uuidSomething went wrong
Ineligible card country of issuance400The card country of issuance (XX) is not supported (subaccount_blocking_card_country)Something went wrong
Incorrect expiry date400Expiry date cannot be in the pastSomething went wrong
Incorrect / Invalid PAN400Invalid PANSomething went wrong
Incorrect / Invalid CVC400CVC must be 3 or 4 digitsSomething went wrong
Card subscription already exists409There is already a card subscription for the specified cardYou're all set
Card not supported400Invalid PANSomething went wrong
3DS challenge failed (provider-specific)400Card verification step with id=challenge failed... 3DS details: '[reason]'Something went wrong

Consent

What is Card Holder Consent?

Card holder consent refers to the approval a card holder gives to allow their transaction data to be accessed and used by third parties like Astrada. This consent is essential for complying with card network requirements and ensuring data security.

Why We Collect Consent

Contractual Requirement from Card Networks

Card networks mandate obtaining card holder consent to ensure that their transaction data is shared responsibly and ethically.

Through Astrada's APIs and SDK, card holders can opt in and authorize the sharing of their data. This opt-in process is crucial for Astrada and our customers to access such data legitimately.

Data Security and Best Practices

Collecting consent ensures that sensitive card holder data is not accessed or shared inappropriately, adhering to stringent data security standards.

How We Collect Consent

Astrada initiates the consent collection process when a customer enrolls a card for the first time.

Consent is gathered through a clear and conspicuous request, ensuring the card holder is fully informed and has the freedom to either consent or refuse. The request will explain the purpose of data collection and the specifics of how data will be used.

We use consent language approved by card networks to ensure uniformity and compliance (see Installation). This language is integrated into our card enrollment SDK by default.

Card holders must agree to Network-specific and Astrada-specific terms separately, ensuring clarity and compliance with privacy laws.

Upon receiving affirmative opt-in consent, Astrada verifies the identity of the card holder to ensure the consent is valid and associated with the correct individual.

We maintain detailed records of consents, including date and time stamps, to comply with legal requirements and for audit purposes.

By integrating with Astrada, our customers ensure that all data is fully compliant with both legal and network requirements.