3D Secure (3DS)¶
Introduction¶
3D Secure (3DS) is an authentication protocol designed to reduce fraud in online card payments. It adds an extra verification step where the cardholder's bank (issuer) authenticates the customer before the payment is authorized. The Payment Gateway supports 3D Secure 2.x, which provides a better user experience through risk-based authentication — many transactions are approved without customer interaction (frictionless flow), while higher-risk transactions require the cardholder to complete a challenge (e.g., entering an OTP or confirming in a banking app).
3DS authentication is a regulatory requirement under PSD2 Strong Customer Authentication (SCA) in the European Economic Area and is widely adopted by card networks globally.
When is 3DS Triggered?¶
| Payment Method | 3DS Behavior |
|---|---|
Card (card) |
3DS is always initiated |
Saved Card (saved_card) |
3DS is always initiated |
Google Pay (google-pay) |
3DS is initiated for PAN_ONLY tokens; skipped for CRYPTOGRAM_3DS tokens |
Network Token (network-token) |
3DS is skipped when a valid cryptogram and ECI are provided |
| OCT | No 3DS — Original Credit Transactions do not require authentication |
| Payout | No 3DS — Payouts do not require authentication |
3DS Flow Overview¶
When a card transaction requires 3DS authentication, the response from the /api/transactions/authorize endpoint will include additional fields that guide the merchant through the authentication process. The flow involves up to two steps: device fingerprinting and challenge authentication.
sequenceDiagram
participant Customer
participant Merchant
participant Gateway as Payment Gateway
participant ACS as Issuer ACS
Merchant ->> Gateway: POST /api/transactions/authorize
Gateway -->> Merchant: Response with action (fingerprint)
Note over Merchant: Step 1 — Device Fingerprint
Merchant ->> Merchant: Decode token from action response
Merchant ->> ACS: POST threeDSMethodData (hidden iframe)
ACS -->> Gateway: Notification (via threeDsMethodNotificationUrl)
Merchant ->> Gateway: POST /api/three-ds/{id}/request-authentication
Gateway -->> Merchant: Response (approved, or challenge required)
alt Frictionless Flow
Note over Merchant: Transaction approved without customer interaction
else Challenge Required
Note over Merchant: Step 2 — Challenge
Merchant ->> Merchant: Decode token, build CReq
Merchant ->> ACS: POST creq to ACS (iframe)
Customer ->> ACS: Complete authentication (OTP, biometric, etc.)
ACS -->> Merchant: CRes via window message
Merchant ->> Gateway: POST /api/three-ds/{id}/authentication-completed
Gateway -->> Merchant: Final transaction result
end
Response Handling¶
After calling /api/transactions/authorize, the response contains four top-level fields. Which fields are populated determines the next step:
| Response Scenario | result |
action |
redirect |
form_submit |
Next Step |
|---|---|---|---|---|---|
| Approved (no 3DS needed) | Transaction data | null |
null |
null |
Done — transaction is approved |
| Declined | Transaction data | null |
null |
null |
Done — transaction is declined |
| Fingerprint required | null |
Action details | null |
null |
Perform device fingerprinting |
| Redirect required | null |
null |
Redirect URL | null |
Redirect customer to URL |
| Form submit required | null |
null |
null |
Form details | Auto-submit form to URL |
Step 1 — Device Fingerprint¶
When the response contains an action object with type: "fingerprint", you must collect the customer's device fingerprint by submitting a hidden form to the issuer's Access Control Server (ACS).
Action Response Structure¶
{
"result": null,
"action": {
"transaction_id": "vvIVFmPuwYosePLsoDsW",
"session_id": "abc123-session-id",
"type": "fingerprint",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"payment_data": "encrypted_payment_data_string"
},
"redirect": null,
"form_submit": null
}
Action Fields¶
| Field | Type | Description |
|---|---|---|
transaction_id |
string |
The transaction identifier |
session_id |
string |
The session identifier for this 3DS flow |
type |
string |
Either "fingerprint" or "challenge" |
token |
string |
Base64-encoded JSON containing ACS URLs and 3DS server transaction data |
payment_data |
string |
Encrypted payment data — must be sent back when completing the transaction |
Decoded Token Structure¶
The token field is a Base64-encoded JSON object with the following structure:
{
"acsUrl": "https://acs.issuer.com/challenge",
"messageVersion": "2.2.0",
"threeDsServerTransId": "abc123-server-trans-id",
"threeDsMethodNotificationUrl": "https://gateway.example.com/three-ds/notification",
"threeDsMethodUrl": "https://acs.issuer.com/fingerprint",
"acsTransID": "xyz789-acs-trans-id",
"challengeWindowSize": "05"
}
| Field | Description |
|---|---|
threeDsMethodUrl |
URL to submit the fingerprint form to |
threeDsServerTransId |
3DS server transaction ID — used in subsequent API calls |
threeDsMethodNotificationUrl |
URL where the ACS will notify the gateway of fingerprint completion |
acsUrl |
ACS URL for challenge step (used in Step 2 if needed) |
acsTransID |
ACS transaction ID (used in Step 2 if needed) |
messageVersion |
3DS protocol version |
challengeWindowSize |
Challenge window size code (used in Step 2 if needed) |
Implementation¶
1. Decode the token:
const decodedToken = JSON.parse(atob(action.token));
2. Build the threeDSMethodData:
Construct a JSON object containing the server transaction ID and notification URL, then Base64 URL-encode it:
const fingerprintData = {
threeDSServerTransID: decodedToken.threeDsServerTransId,
threeDSMethodNotificationURL: decodedToken.threeDsMethodNotificationUrl
};
const threeDSMethodData = btoa(JSON.stringify(fingerprintData));
3. Create and auto-submit the hidden form:
<iframe id="fingerprint-iframe" name="fingerprint-iframe"
style="display:none;" width="0" height="0"></iframe>
<form id="fingerprint-form" method="POST"
action="<decodedToken.threeDsMethodUrl>"
target="fingerprint-iframe">
<input type="hidden" name="threeDSMethodData"
value="<threeDSMethodData>" />
</form>
<script>
document.getElementById('fingerprint-form').submit();
</script>
4. Wait for the fingerprint to complete (timeout: ~5 seconds), then proceed to request authentication.
Requesting Authentication¶
After the fingerprint step, request authentication by calling the 3DS authentication endpoint:
POST /api/three-ds/{threeDsServerTransId}/request-authentication
Authorization: Bearer <access_token>
Content-Type: application/json
{
"fingerprint_result": "<base64-encoded completion indicator>",
"payment_data": "<payment_data from the action response>"
}
The response will be in the same format as the initial authorize response:
- If the transaction is approved (frictionless flow):
resultwill contain the transaction data - If a challenge is required: the response will contain a new
actionwithtype: "challenge", aredirect, or aform_submit
Step 2 — Challenge¶
If the issuer requires additional customer authentication after the fingerprint step, the response will indicate a challenge. This can be returned as an action with type: "challenge", a redirect, or a form_submit.
Action with Challenge Type¶
When the response contains an action with type: "challenge":
{
"result": null,
"action": {
"transaction_id": "vvIVFmPuwYosePLsoDsW",
"session_id": "abc123-session-id",
"type": "challenge",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"payment_data": "encrypted_payment_data_string"
},
"redirect": null,
"form_submit": null
}
1. Decode the token (same structure as the fingerprint token):
const decodedToken = JSON.parse(atob(action.token));
2. Build the Challenge Request (CReq):
const creq = {
threeDSServerTransID: decodedToken.threeDsServerTransId,
acsTransID: decodedToken.acsTransID,
messageVersion: decodedToken.messageVersion,
messageType: "CReq",
challengeWindowSize: decodedToken.challengeWindowSize
};
const encodedCReq = btoa(JSON.stringify(creq));
3. Submit the CReq to the ACS via an iframe:
<iframe id="challenge-iframe" name="challenge-iframe"
width="100%" height="400"></iframe>
<form id="challenge-form" method="POST"
action="<decodedToken.acsUrl>"
target="challenge-iframe">
<input type="hidden" name="creq" value="<encodedCReq>" />
</form>
<script>
document.getElementById('challenge-form').submit();
</script>
The customer will see the issuer's authentication UI inside the iframe (e.g., OTP input, biometric prompt).
4. Listen for the Challenge Response (CRes):
The ACS sends the challenge result back via a window.message event:
window.addEventListener('message', function(event) {
const data = JSON.parse(event.data);
if (data.type === 'CHALLENGE_RESPONSE') {
const transStatus = data.trans_status;
// Proceed to submit the result to the gateway
}
});
Transaction Status Values (trans_status)¶
| Value | Meaning |
|---|---|
Y |
Authentication successful |
N |
Not authenticated (denied) |
U |
Authentication could not be performed |
A |
Authentication attempted |
C |
Challenge required (additional interaction needed) |
R |
Authentication rejected |
Completing the Challenge¶
After receiving the CRes, submit the authentication result to the gateway:
POST /api/three-ds/{threeDsServerTransId}/authentication-completed
Authorization: Bearer <access_token>
Content-Type: application/json
{
"payment_data": "<payment_data from the action response>",
"session_id": "<session_id from the action response>",
"trans_status": "<trans_status from CRes>",
"parent_transaction_id": "<transaction_id from the action response>"
}
The response will contain the final transaction result.
Redirect Flow¶
When the response contains a redirect object instead of an action:
{
"result": null,
"action": null,
"redirect": {
"transaction_id": "vvIVFmPuwYosePLsoDsW",
"session_id": "abc123-session-id",
"url": "https://acs.issuer.com/challenge?id=..."
},
"form_submit": null
}
Redirect the customer's browser to the provided url. After authentication, the customer will be redirected back to your return_url.
Form Submit Flow¶
When the response contains a form_submit object:
{
"result": null,
"action": null,
"redirect": null,
"form_submit": {
"transaction_id": "vvIVFmPuwYosePLsoDsW",
"session_id": "abc123-session-id",
"url": "https://acs.issuer.com/challenge",
"data": {
"PaReq": "eJxVUt1ugjAUvl...",
"MD": "vvIVFmPuwYosePLsoDsW",
"TermUrl": "https://gateway.example.com/callback"
}
}
}
Create a form with the provided fields and auto-submit it:
<form id="challenge-form" method="POST"
action="https://acs.issuer.com/challenge">
<input type="hidden" name="PaReq" value="eJxVUt1ugjAUvl..." />
<input type="hidden" name="MD" value="vvIVFmPuwYosePLsoDsW" />
<input type="hidden" name="TermUrl" value="https://gateway.example.com/callback" />
</form>
<script>
document.getElementById('challenge-form').submit();
</script>
After authentication, the customer will be redirected back to your return_url.
After Authentication¶
Once the customer completes the 3DS challenge (via redirect or form submit flows), they will be redirected back to your return_url. Verify the transaction status:
GET /api/transactions/{transaction_id}/status
Authorization: Bearer <access_token>
The transaction will be in one of the following statuses:
| Status | Description |
|---|---|
APPROVED |
Authentication successful, transaction authorized |
DECLINED |
Authentication failed or transaction was declined by the issuer |
CANCELED |
Customer canceled the authentication |
SESSION_EXPIRED |
The 3DS session timed out before the customer completed authentication |
Use Webhooks
Instead of polling the transaction status, configure a webhook to receive real-time notifications when the transaction reaches a final status.
Browser Info¶
The browser_info object is required for all card transactions and is used by the issuer to assess risk during 3DS authentication. You must collect this information from the customer's browser.
Required Fields¶
| Field | Type | Description | Example |
|---|---|---|---|
user_agent |
string |
The browser's User-Agent header | "Mozilla/5.0..." |
accept_header |
string |
The browser's Accept header | "text/html,application/xhtml+xml..." |
java_enabled |
boolean |
Whether Java is enabled | false |
color_depth |
integer |
Screen color depth in bits | 24 |
screen_height |
integer |
Screen height in pixels | 1080 |
screen_width |
integer |
Screen width in pixels | 1920 |
time_zone_offset |
integer |
Timezone offset from UTC in minutes | -120 |
language |
string |
Browser language | "en-US" |
JavaScript Collection Example¶
const browserInfo = {
user_agent: navigator.userAgent,
accept_header: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
java_enabled: navigator.javaEnabled(),
color_depth: screen.colorDepth,
screen_height: screen.height,
screen_width: screen.width,
time_zone_offset: new Date().getTimezoneOffset(),
language: navigator.language
};
Accept Header
The accept_header value cannot be read directly from JavaScript. Use a standard value as shown above, or capture it server-side from the customer's HTTP request headers.
Timeouts¶
| Step | Timeout | Description |
|---|---|---|
| Device fingerprint | 5 seconds | If the ACS does not respond within this time, proceed to request authentication regardless |
| Challenge | 10 minutes | Maximum time allowed for the customer to complete the challenge |
Complete Integration Example¶
Below is a simplified example of the full 3DS flow for a card payment:
sequenceDiagram
participant Browser as Customer Browser
participant Server as Merchant Server
participant Gateway as Payment Gateway
participant ACS as Issuer ACS
Browser ->> Server: Checkout (card details + browser info)
Server ->> Gateway: POST /api/transactions/authorize
Gateway -->> Server: action (type: fingerprint, token, payment_data)
Server -->> Browser: Decoded token + payment_data
Note over Browser: Step 1 — Device Fingerprint
Browser ->> Browser: Build threeDSMethodData from token
Browser ->> ACS: POST threeDSMethodData (hidden iframe to threeDsMethodUrl)
Note over Browser: Wait up to 5 seconds
Browser ->> Server: Fingerprint complete
Server ->> Gateway: POST /api/three-ds/{id}/request-authentication
Gateway -->> Server: Frictionless approval or challenge action
alt Approved (Frictionless)
Server -->> Browser: Payment successful
else Challenge Required (action)
Server -->> Browser: Challenge token + payment_data
Note over Browser: Step 2 — Challenge
Browser ->> Browser: Build CReq from token
Browser ->> ACS: POST creq (iframe to acsUrl)
Browser ->> ACS: Customer completes authentication
ACS -->> Browser: CRes via window.message
Browser ->> Server: trans_status from CRes
Server ->> Gateway: POST /api/three-ds/{id}/authentication-completed
Gateway -->> Server: Final transaction result
Server -->> Browser: Payment result
else Redirect Required
Server -->> Browser: Redirect URL
Browser ->> ACS: Browser redirect
ACS -->> Browser: Redirect to return_url
Browser ->> Server: Return from authentication
Server ->> Gateway: GET /api/transactions/{id}/status
Gateway -->> Server: Final status
Server -->> Browser: Payment result
end
Related Documentation¶
- Card Authorization — Card payment overview
- API Integration — Direct API integration guide
- Google Pay — Google Pay integration with 3DS details
- Network Token — Network token payments (3DS pre-authenticated)
- Transaction Statuses — All transaction lifecycle states
- Webhooks — Receive real-time payment notifications