Skip to main content

Credit Card Payment with 3DS

Prerequisite

Contact your relationship manager to obtain the values for orgUnitId, iss, and secret before starting this integration.

Credit card payment in the Direct Model requires four steps. Three parties are involved: your application (frontend and backend), BBMSL, and Cardinal (the 3DS verification service provider). Payment data is exchanged securely across all three parties.

The diagram below shows the complete flow for reference.

Docusaurus

1. 3DS session initialization

What you do: Initialize a 3DS session with Cardinal by posting the card BIN (first 6–9 digits) and a signed JWT to the Collect endpoint via an invisible iframe. Cardinal responds with a SessionId via a JavaScript postMessage.

Expected result: Cardinal returns a SessionId delivered to your page via a JavaScript postMessage event.

Next step: Pass the SessionId to PayAPI Auth in Step 2.

You need three static values to complete this step: orgUnitId, iss, and secret.

Place an invisible iframe element on your page to make an HTTP request to Cardinal. The API details are as follows:

  • URL: https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect
  • Method: POST
  • Query Parameters:
ParameterTypeMandatoryDescription
BinStringMThe card number (PAN). Minimum of first 6-digits
JWTStringMGenerated JWT token
Bin

We strongly recommend providing the first 9-digits of the PAN, which may increase the authentication success rate.

To generate the JWT token, use the following sample code with JJWT or any compatible library for signing the token.

generateJwt.java
public String generateJwt(String orgUnitId, String iss, String secret) {
try {
return Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setIssuer(iss)
.claim("OrgUnitId", orgUnitId)
.signWith(SignatureAlgorithm.HS256, secret.getBytes("UTF-8"))
.compact();
} catch (UnsupportedEncodingException e) {
return null;
}
}

Below is an example of requesting Cardinal to submit a form using the above values.

<!-- This is a Cardinal Commerce URL in live. -->
<form id="collectionForm" method="POST" action="https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect">
<input type="hidden" name="Bin" value="4000000000001000" />
<input type="hidden" name="JWT" value="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2OWFkYzE4NS0xNzQ4LTQ1MjUtOWVmOS00M2YyNTlhMWMyZDYiLCJpYXQiOjE1NDg4Mzg4NTUsImlzcyI6IjViZDllMGU0NDQ0ZGNlMTUzNDI4Yzk0MCIsIk9yZ1VuaXRJZCI6IjViZDliNTVlNDQ0NDc2MWFjMGFmMWM4MCJ9.qTyYn4rItMMNdnh6ouqW6ZmcCNzaG9JI_GdWGIaq6rY" />
</form>
<script>
window.onload = function() {
document.getElementById('collectionForm').submit();
}
</script>
warning

You should embed the above code into an invisible iframe element to request the Cardinal API instead of directly requesting through non-UI code.

You can either use src or srcdoc attribute depending on your implementation,

<iframe src="/path_to_the_file.html" height="1" width="1" style="display: none;"/>
<iframe srcdoc="<p>Hello world!</p>" height="1" width="1" style="display: none;"/>

After submitting the form, Cardinal notifies your page via a JavaScript postMessage. Listen for this message to retrieve the SessionId value before proceeding to Step 2.

Here is an example of listening for the event and extracting SessionId from the postMessage.

window.addEventListener("message", function(event) {
//This is a Cardinal Commerce URL in live.
if (event.origin === "https://centinelapistag.cardinalcommerce.com") {
var data = JSON.parse(event.data);
console.warn('Merchant received a message:', data);
if (data !== undefined && data.Status) {
// Extract the value of SessionId for onward processing.
}
}
}, false);
Example postMessage
{
"MessageType": "profile.completed",
"SessionId": "d3197c02-6f63-4ab2-801c-83633d097e32",
"Status": true
}
Event listener

Place the event listener outside the iframe to ensure the event is captured.

2. PayAPI: Auth

What you do: Pass the SessionId from Step 1 in the ddcSession field when calling PayAPI Auth: /direct/auth. BBMSL forwards the request to the card issuer, which may require additional authentication from the cardholder.

Expected result: The response responseCode determines the next action.

Next step: If responseCode is 0000, the payment is complete — no further action is needed. If 5000, proceed to Step 3.

Response CodeDescriptionAction required
0000Payment successNo further action required
50003DS Challenge Required, additional object threeDSChallengeDetails will be returned in the response.Follow the steps below to complete 3DS Challenge
OthersPayment failed with errorRetry after tackling the error

As shown above, if the response code is 0000 the payment has already succeeded and no further action is required.

3. 3DS challenge

What you do: Display a 3DS challenge webpage from the issuing bank so the cardholder can complete the additional authentication required by the issuer.

Expected result: The customer completes the verification, and the browser redirects to your returnUrl with stan and orderId as query parameters.

Next step: Pass stan and orderId to PayAPI Complete Authentication in Step 4.

If the Auth API responds with 5000, the issuing party requires extra authentication from the cardholder. Request the 3DS challenge page from Cardinal using the details below.

  • URL: https://centinelapistag.cardinalcommerce.com/V2/Cruise/StepUp
  • Method: POST
  • Query Parameters:
ParameterTypeMandatoryDescription
JWTStringMGenerated JWT token

Request URL example

https://centinelapistag.cardinalcommerce.com/V2/Cruise/StepUp?JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkOTcyYWZlNi1kMmRmLTQ3MGEtOTFiYy1mZjY3NWM4NjY2ZTIiLCJpYXQiOjE2NTgxMzA2ODIsImlzcyI6IjYwZWMwNDQzNGJmNTdkMDdhZDM4NjlkOCIsIk9yZ1VuaXRJZCI6IjYwZWMwNDQzNGJmNTdkMDdhZDM4NjlkNyIsIlJldHVyblVybCI6Imh0dHBzOi8vd3d3LmJibXNsLmNvbS9yZWRpcmVjdD9zdGFuPVM0NTU2Jm9yZGVySWQ9NjU5MCIsIlBheWxvYWQiOnsiUGF5bG9hZCI6ImVOcFZVVjF2Z2pBVWZlK3ZJTXVlYWFrZzAxeWI2TWpVVFJZejNiTHRyVUl6TVZDd2dNaS9YNHRmMjlzOTkvT2NjMkc5VlVJRUt4SFZTakFJUlZueUgyRWw4ZWh1UWZzNzhYUm9wNGYranFvaUxKMlUzREZZanQvRW5zRkJxRExKSlhOc1lsUEFGNGowQ2hWdHVhd1k4R2cvbWI4eTEzVWR4d0Y4aGdneW9lWUJDOTdEOEF2d0NTQ1FQQk5zTWdsWEMydkoyMHpJeXByeVNqUzhCZHpWRUVSNUxTdlZNdC9UQnk4QVFhMVN0cTJxWW9oeDB6VDJacE9WcVIzbEdXQlRRWUJ2akphMWlVb3Q5SmpFYkIzTUtjOCtwckZNWi9IdWV4L0s1MmI5R1I0M3dYZ0UySFFnaURVSFJnbWx4SGNlTE9JUDNjSFFkUUYzZVFROE16VFk3T1hlSXpZaFd1UXBnYUF3cDhZbjVCRlQrcHZSWW1xbGhJeGExalBycmdpQk9CYTUxUEtaRm5tTnRZb2I5OGVaOFRhcXRHK2VQNkJrNEJwek85eU5KOW9XMm5OSU41OTBIbUV6ZzgrdncrY3Y2K2pmOTM4QnBaK3Fkdz09IiwiQUNTVXJsIjoiaHR0cHM6Ly9tZXJjaGFudGFjc3N0YWcuY2FyZGluYWxjb21tZXJjZS5jb20vTWVyY2hhbnRBQ1NXZWIvcGFyZXEuanNwP3ZhYT1iJmdvbGQ9QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBIiwiVHJhbnNhY3Rpb25JZCI6IkwyNmplRnZ5R3Y2ajJycE1zMWwwIn0sIk9iamVjdGlmeVBheWxvYWQiOnRydWV9.hPPwSYaSGlb03PXK3hidTyhkIvK99Oe9-ZlGrDxq48M
Request Parameter

The request parameters are constructed as query parameters instead of the request body as shown in the example.

Similar to the 3DS Collect step, a signed JWT token is required to obtain the 3DS challenge session from Cardinal. Map the threeDSChallengeDetails from the Auth response to HashMap format. The returnUrl is where the browser redirects after the customer completes the 3DS challenge; stan and orderId are appended as query parameters. See the sample code below.

generateJwt.java
public String generateStepUpJwt(String orgUnitId, String iss, String secret, String threeDSDetails, String stan, int orderId, String returnBaseUrl) {
String returnUrl = returnBaseUrl + "?stan=" + stan;
returnUrl += "&orderId=" + orderId;

try {
JSONObject object = new JSONObject(threeDSDetails);

HashMap<String, String> payloadMap = new HashMap<>();
payloadMap.put("ACSUrl", object.getString("acsURL"));
payloadMap.put("Payload",object.getString("payload"));
payloadMap.put("TransactionId",object.getString("transactionId3DS"));

return Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setIssuer(iss)
.claim("OrgUnitId", orgUnitId)
.claim("ReturnUrl", returnUrl)
.claim("Payload", payloadMap)
.claim("ObjectifyPayload", true)
.signWith(SignatureAlgorithm.HS256, secret.getBytes("UTF-8"))
.compact();
} catch (UnsupportedEncodingException e) {
return null;
}
}

Cardinal returns a 3DS challenge webpage from the issuing bank. Display this HTML page directly in your application for the customer to complete authentication.

Example 3DS challenge webpage response
<!DOCTYPE html>
<html>

<head>
<title>Cruise API - Step Up</title>

<style>
* {
margin: 0;
padding: 0;
}

.hide {
display: none;
}

#stepUpView {
width: 100%;
height: 100%
}

#stepUpView iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

#loadingImage {
height: 30px;
width: 30px;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
transform: -webkit-translate(-50%, -50%);
transform: -moz-translate(-50%, -50%);
transform: -ms-translate(-50%, -50%);
}

.outOfview {
position: absolute;
left: -2000px;
top: -2000px;
}
</style>
</head>

<body>

<div id="stepUpView"></div>

<div class="hide">
<input id="acsUrl" name="acsUrl" value="https://merchantacsstag.cardinalcommerce.com/MerchantACSWeb/pareq.jsp?vaa=b&amp;gold=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" type="hidden"/>
<input id="payload" name="payload" value="eNpVUV1vgjAUfe+vIMueaakg01yb6MjUTRYz3bLtrUIzMVCwgMi/X4tf29s99/Occ2G9VUIEKxHVSjAIRVnyH2El8ehuQfs78XRop4f+jqoiLJ2U3DFYjt/EnsFBqDLJJXNsYlPAF4j0ChVtuawY8Gg/mb8y13UdxwF8hggyoeYBC97D8AvwCSCQPBNsMglXC2vJ20zIyprySjS8BdzVEER5LSvVMt/TBy8AQa1Stq2qYohx0zT2ZpOVqR3lGWBTQYBvjJa1iUot9JjEbB3MKc8+prFMZ/Huex/K52b9GR43wXgE2HQgiDUHRgmlxHceLOIP3cHQdQF3eQQ8MzTY7OXeIzYhWuQpgaAwp8Yn5BFT+pvRYmqlhIxa1jPrrgiBOBa51PKZFnmNtYob98eZ8TaqtG+eP6Bk4BpzO9yNJ9oW2nNIN590HmEzg8+vw+cv6+jf938BpZ+qdw==" type="hidden"/>
<input id="mcsId" name="mcsId" value="0_7a3e6371-22e3-4e12-8abf-050ecdeff15c" type="hidden"/>
<input id="termUrl" name="termUrl" value="https://centinelapistag.cardinalcommerce.com/V1/TermURL/Overlay/CCA" type="hidden"/>
<input id="threeDSVersion" name="threeDSVersion" value="1" type="hidden"/>
<input id="baseUrl" name="baseUrl" value="https://centinelapistag.cardinalcommerce.com" type="hidden"/>
<input id="orgUnitId" name="orgUnitId" value="60ec04434bf57d07ad3869d7" type="hidden"/>
<input id="transactionId" name="transactionId" value="L26jeFvyGv6j2rpMs1l0" type="hidden"/>
<input id="isPostMessageEventsEnabled" name="isPostMessageEventsEnabled" value="false" type="hidden"/>
</div>

<div class="hide">
<form id="redirect" method="POST"
action="https://centinelapistag.cardinalcommerce.com/V1/Cruise/TermRedirection">
<input type="hidden" name="McsId" id="redirect-mcsId" value="0_7a3e6371-22e3-4e12-8abf-050ecdeff15c"/>
<input type="hidden" name="CardinalJWT" id="CardinalJWT"/>
<input type="hidden" name="Error" id="Error"/>
</form>
</div>

<script src="https://centinelapistag.cardinalcommerce.com/javascript/vendors.83e0876bb78df21a4fcc.js"></script>
<script src="https://centinelapistag.cardinalcommerce.com/javascript/stepUp.e327e52d1f61c8e6cc84.js"></script>
<script>
window.onload = function () {
try {
if (window.CruiseAPI !== undefined) {
var configuration = {
acsUrl: document.getElementById('acsUrl').value,
baseUrl: document.getElementById('baseUrl').value,
encodedMcsId: document.getElementById('mcsId').value,
mcsId: document.getElementById('redirect-mcsId').value,
orgUnitId: document.getElementById('orgUnitId').value,
payload: document.getElementById('payload').value,
termUrl: document.getElementById('termUrl').value,
threeDSVersion: document.getElementById('threeDSVersion').value,
transactionId: document.getElementById('transactionId').value,
isPostMessageEventsEnabled: document.getElementById('isPostMessageEventsEnabled').value,
};

CruiseAPI.stepUp.run(configuration)
} else {
console.error('Global not found, unable to complete step up');
document.getElementById("Error").value = "JS Global not found";
document.getElementById("redirect").submit();
}
}catch(error) {
if(window.CruiseAPI !== undefined){
CruiseAPI.stepUp.handleError(error.message);
} else {
console.error(error);
}
}
}
</script>
</body>

</html>

After the customer completes verification, the browser redirects to the provided returnUrl with stan and orderId as query parameters. An example redirect for returnUrl='https://merchant-owned-website.com/return' is shown below.

https://merchant-owned-website.com/return?stan=S4556&orderId=6590

4. PayAPI: Complete Authentication

What you do: Notify BBMSL that the 3DS challenge is complete by calling PayAPI Complete Authentication: /direct/complete-authentication with the stan and orderId values from the returnUrl query parameters.

Expected result: BBMSL records the completed order. A payment success notification is sent to your backend notifyUrl if one was provided.

Next step: The payment process is complete. Handle the result in your application workflow. See Result Notification for notification handling details.