Direct-API Card Enrollment Integration
Introduction
This page is the direct-API integration reference for partners who have been granted permission to enroll cards without using the Astrada Card Enrollment SDK. If you are not sure whether your integration qualifies for direct-API access, talk to your Astrada contact before relying on this page — most integrators should use the SDK, which handles every response path described below.
The page is the contract you need to implement: every request shape, every response shape, every status code, and the literal validation messages your error handler will see.
Customers with personal cards or business cards can add their cards individually. Cardholders are required to complete a verification process to ensure they own the card when they link it.
How enrollment actually completes
Card enrollment always ends in one of these states on the creation response of POST /card-verifications/3ds:
state | currentStepId | What the client should do next |
|---|---|---|
in-progress | "fingerprint" | Follow _links.currentStep.href to the fingerprint step. 3DS is needed. |
completed | null | Done. The card is enrolled. Do not call any /steps/... endpoint. |
| — | — | Errors are documented in the Errors section below. |
The key insight: not every enrollment needs a fingerprint or challenge. If the card issuer supports frictionless authentication (or the card does not require interactive 3DS), you'll receive a creation response with state: "completed" and currentStepId: null. In that case the enrollment is already finished — calling GET /card-verifications/3ds/{verificationId}/steps/fingerprint would return 404 because no step row exists.
Client-side branching rule
The rule below applies only to the POST /card-verifications/3ds creation response. Do not reuse it for GET /card-verifications/3ds/{verificationId} polling — see Polling a verification for why.
// Handle the POST /card-verifications/3ds response.
if (response.state === "completed" || response.currentStepId === null) {
// Enrolled. No further action needed.
} else if (response._links && response._links.currentStep) {
// 3DS is required. Follow the href — don't hardcode /steps/fingerprint.
await handleStep(response._links.currentStep.href);
}Polling a verification
If a client abandons the flow mid-way (closed tab, hung iframe), the verification transitions to state: "failed" roughly one hour after creation via a scheduled check (one per verification, cancelled on step completion). To detect this, call GET /card-verifications/3ds/{verificationId} and branch on the full state taxonomy:
state value | Meaning |
|---|---|
in-progress | Verification is still pending a step. Continue or wait. |
completed | Verification succeeded. Card is enrolled. |
failed | Verification failed (a step explicitly failed, or the flow was abandoned and expired). Start a new verification if the cardholder is still available. |
On the creation response, state is only ever in-progress or completed — never failed. failed appears only on subsequent reads.
To list every step that has been recorded for a verification (fingerprint, challenge, or both), follow the _links.steps.href on the verification resource — equivalent to GET /card-verifications/3ds/{verificationId}/steps. Useful when reconciling state after a partial flow.
Single Card Enrollment Walkthrough
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. Create the card subscription
The second stage of the card-linking journey requires the collection of card data and consent. At this stage the user provides sensitive data and opts into the terms of the data-sharing arrangement explicitly.
Subscription request
POST https://api.astrada.co/card-subscriptions{
"subaccountId": "<SUBACCOUNT_ID>",
"country": "<ISO_3166_ALPHA3>",
"expiryMonth": "<EXP_MONTH>",
"expiryYear": "<EXP_YEAR>",
"pan": "<CARD_NUMBER>",
"cvc": "<CVC_NUMBER>",
"cardholderName": "<HOLDER_NAME>",
"customerReferenceId": "<CUSTOMER_REFERENCE_UUID>"
}customerReferenceId is optional. When provided, it must be a UUID; use it to correlate this subscription with an entity in your own system.
Response states
The POST /card-subscriptions call can return one of two outcomes. Branch on the response before deciding your next step.
| Status | state | Meaning | Next action |
|---|---|---|---|
201 | reqSCA | The subscription was created and a 3DS verification is required. | Proceed to Start card verification. |
409 | — | An active subscription already exists for this card + subaccount. | GET /card-subscriptions/{id} for the existing subscription and branch on its current state. Do not retry the POST. |
The subscription is always created in reqSCA on success — it transitions to active only after the verification flow below completes.
3. Start card verification
For subscriptions that require SCA, start the verification:
Verification request
POST https://api.astrada.co/card-verifications/3ds{
"subaccountId": "<SUBACCOUNT_ID>",
"expiryMonth": "<EXP_MONTH>",
"expiryYear": "<EXP_YEAR>",
"pan": "<CARD_NUMBER>",
"cvc": "<CVC_NUMBER>",
"cardholderName": "<HOLDER_NAME>"
}The verification create body does not accept country (that field belongs only to POST /card-subscriptions).
Interpret the response
Use the decision table at the top of this page. Two examples:
Frictionless — no further steps needed:
{
"id": "9fab1bea-bc1e-4757-bd47-479422e5983b",
"subaccountId": "f297d659-c13d-4219-aeaa-e10a845140a5",
"cardId": "8309b5f8-d5d8-49bb-9001-38bf1bb0f1e4",
"type": "3DS",
"currentStepId": null,
"state": "completed",
"createdAt": "2024-01-04T18:53:32.000Z",
"updatedAt": "2024-01-04T18:53:32.000Z",
"_links": {
"self": {
"href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b"
},
"steps": {
"href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps"
}
}
}When you see this, the card is enrolled. Do not follow with GET /steps/fingerprint — no step row was created and you will receive a 404.
3DS required — follow _links.currentStep:
{
"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": "2024-01-04T18:53:32.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"
}
}
}Continue to the fingerprint step.
If you want to check the card verification status on an existing subscription, use
GET /card-verifications/3ds/{verificationId}. See Polling a verification above for the fullstatetaxonomy.
3.1 Device Fingerprint
The first sub-step is to collect device and browser data for frictionless authentication. Retrieve the fingerprint step:
GET https://api.astrada.co/card-verifications/3ds/{verificationId}/steps/fingerprint{
"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/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/fingerprint"
},
"nextStep": {
"href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/challenge"
}
}
}Use the data properties to open an iframe and collect browser information. The hidden iframe POSTs to the ACS (Access Control System) and posts a message to the window (threeDSMethodNotificationURL) when complete.
A small number of TNS-routed cards return the field as
threeDsMethodUrl(lowercasesandUrl) instead of the spec-canonicalthreeDSMethodURL. Consume both casings defensively if you support Mastercard.
<html>
<head>
<script src="/static/fingerprint.js"></script>
<script>
window.onload = function() {
doFingerprint(
'{{ threeDSMethodURL }}',
'{{ threeDSMethodNotificationURL }}',
'{{ threeDSMethodData }}',
'{{ threeDSServerTransID }}');
}
</script>
</head>
</html>// 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) {
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/{verificationId}/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, send the browser details to the ACS so the challenge iframe can be correctly sized:
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"
}
}The server derives ip from the request (X-Forwarded-For or sourceIp); any value sent in the body is ignored.
Fingerprint step outcome values
outcome valuesAfter a successful POST, the fingerprint step's outcome field tells you whether a challenge is still needed:
outcome | Meaning | Next step |
|---|---|---|
authenticated | Frictionless success. Verification state becomes completed. | Done. Card is enrolled. |
requires-challenge | Additional cardholder interaction needed. | Follow _links.nextStep.href to the 3DS Challenge. |
A third value, null, exists in the schema but is never returned by the POST itself — it only appears on a GET of a step that was created and then never POST-completed (for example, an in-progress step or one that the 1-hour scheduled check expired before completion).
outcome vs. state: outcome describes the 3DS result; state describes the step's lifecycle. To detect failure, branch on state === "failed", not on outcome. There is no failed outcome value.
authenticated response:
{
"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/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/fingerprint"
}
}
}requires-challenge response:
{
"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/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/fingerprint"
},
"nextStep": {
"href": "/card-verifications/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/challenge"
}
}
}3.2 3DS Challenge
When the fingerprint response indicates outcome: "requires-challenge", the cardholder needs to complete a token-based challenge (typically a push notification from their bank's app or an SMS code).
The challenge is delivered to the registered cardholder by their issuer. The iframe is fully controlled by the card-verification provider, so use the URL they provide.
Retrieve the challenge step 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/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/challenge"
}
}
}Display the challenge iframe using the acsUrl and encodedCReq:
<html>
<head>
<script src="/static/challenge.js"></script>
<script>
window.onload = function() {
doChallenge('{{ acsUrl }}', '{{ encodedCReq }}');
}
</script>
</head>
<body>
Performing 3DS challenge.
</body>
</html>// 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/{verificationId}/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);
};When the challenge iframe completes, it posts a message to the window. On that message, POST to the challenge step to finalize the verification:
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/3ds/9fab1bea-bc1e-4757-bd47-479422e5983b/steps/challenge"
}
}
}409 on challenge POST
The challenge POST can return 409 Conflict for two reasons:
- The user abandoned the verification during the authentication process.
- The user did not complete the 3DS challenge during the authentication process.
In both cases the client should start a new verification rather than retrying the same one. The 409 response body carries a human-readable detail explaining which of the two occurred.
4. Check the final verification state
To verify the terminal state of a card verification, call:
GET https://api.astrada.co/card-verifications/3ds/{verificationId}state will be completed or failed. The exact meanings are in Polling a verification above.
Errors
Card enrollment (POST /card-subscriptions)
POST /card-subscriptions)| Validation | HTTP status | API message detail |
|---|---|---|
subaccountId is not a valid UUID | 400 | Invalid uuid |
country is not a valid ISO 3166 Alpha-3 code | 400 | Country code must be in ISO 3166 Alpha3 format |
| Card country of issuance blocked for the subaccount | 403 | The card country of issuance (XX) is not supported |
expiryYear is not a 4-digit year | 400 | expiryYear must be in YYYY format |
| Expiry date is in the past | 400 | Expiry date cannot be in the past |
pan failed validation (invalid PAN, unsupported network, or BIN check) | 400 | Invalid PAN |
cvc is not 3 or 4 digits | 400 | CVC must be 3 or 4 digits |
| Card subscription already exists for this card + subaccount | 409 | There is already a card subscription for the specified card |
| Unhandled service error | 500 | An unexpected error happened while creating a card subscription |
400 validation responses come back as a
Bad Requestproblem+json envelope. Each per-field message in the table above appears inerrors[i].detail, witherrors[i].instancepointing at the failing path (for example/body/pan).
Card verification (POST /card-verifications/3ds/.../steps/challenge)
POST /card-verifications/3ds/.../steps/challenge)| Validation | HTTP status | API message detail |
|---|---|---|
| Verification abandoned by the user | 409 | Card verification with id=X was abandoned by the user during the authentication process. Please start a new verification. |
| 3DS challenge not completed by the user | 409 | The 3DS challenge for Card verification with id=X was not completed by the user during the authentication process. Please start a new verification. |
| Challenge step failed (3DS provider returned an error) | 500 | Card verification step with id=challenge failed for card verification with id=X. 3DS details: '[reason]'. Please start a new verification. |
Consent
What is Cardholder Consent?
Cardholder consent is the approval a cardholder 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 cardholder consent to ensure that transaction data is shared responsibly and ethically.
Through Astrada's APIs and SDK, cardholders 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 cardholder 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 cardholder is fully informed and has the freedom to consent or refuse. The request explains 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 the Web SDK docs). This language is integrated into our Card Enrollment SDK by default.
Cardholders 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 cardholder 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.
Updated 1 day ago