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 (THREE_DS_2_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
alt Final Result
Gateway -->> Merchant: Transaction result (approved or declined)
else Additional Authentication Required
Gateway -->> Merchant: Response with new action (THREE_DS_2_FINGERPRINT or THREE_DS_2_CHALLENGE)
Note over Merchant: Repeat 3DS flow from the appropriate step
end
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: "THREE_DS_2_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": "THREE_DS_2_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 |
The action type — see Action Type Values below |
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 |
Action Type Values¶
The type field uses the following values:
| Value | Meaning | Next Step |
|---|---|---|
THREE_DS_2_FINGERPRINT |
Device fingerprint collection required | Perform device fingerprinting |
THREE_DS_2_CHALLENGE |
3DS challenge authentication required | Perform challenge authentication |
Match on action type correctly
Use the exact values above when checking the type field. For example, use action.type === "THREE_DS_2_FINGERPRINT" and action.type === "THREE_DS_2_CHALLENGE" in your implementation. Do not check for shortened values like "fingerprint" or "challenge".
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 uses the same structure as the initial /api/transactions/authorize response. Check the top-level fields to determine the next step:
- If
resultis present — the transaction is finalized (frictionless flow: approved or declined) - If
actionis present withtype: "THREE_DS_2_CHALLENGE"— proceed to challenge authentication - If
redirectis present — redirect the customer - If
form_submitis present — auto-submit the form
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: "THREE_DS_2_CHALLENGE", a redirect, or a form_submit.
Action with Challenge Type¶
When the response contains an action with type: "THREE_DS_2_CHALLENGE":
{
"result": null,
"action": {
"transaction_id": "vvIVFmPuwYosePLsoDsW",
"session_id": "abc123-session-id",
"type": "THREE_DS_2_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 follows the same structure as the initial /api/transactions/authorize response — check the result, action, redirect, and form_submit fields to determine the next step.
Additional authentication may be required
The /authentication-completed endpoint does not always return a final transaction result. If the issuer determines that further authentication is needed, the response will contain a new action, redirect, or form_submit — just like the initial authorization response.
When this happens, handle the response exactly as you would after the initial /api/transactions/authorize call:
- If
actionis present withtype: "THREE_DS_2_FINGERPRINT"— perform device fingerprinting again - If
actionis present withtype: "THREE_DS_2_CHALLENGE"— perform challenge authentication again - If
redirectis present — redirect the customer - If
form_submitis present — auto-submit the form - If
resultis present — the transaction is finalized (approved or declined)
This additional authentication round can occur at most once per authentication attempt. If the issuer requests further authentication after the second round, the transaction will be automatically declined. However, if the transaction is declined and the gateway auto-retries with a different provider, a new 3DS flow may be initiated — see Limits for details.
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.
Handling Repeated Authentication¶
In certain scenarios, the Payment Gateway may return an action, redirect, or form_submit response after you have already completed a 3DS authentication step. This means the issuer or the gateway requires additional authentication before the transaction can be finalized.
When Does This Happen?¶
After completing a challenge (/authentication-completed):
The issuer may request an additional authentication round. For example, after submitting the challenge result with trans_status: "C", the gateway may return a new action with type: "THREE_DS_2_FINGERPRINT" or type: "THREE_DS_2_CHALLENGE".
After a transaction is declined and automatically retried:
When a transaction is declined, the Payment Gateway may automatically retry the transaction with a different payment provider. This retry goes through a full authorization flow, which includes 3DS authentication. As a result, you may receive a new action, redirect, or form_submit response that requires the customer to complete 3DS authentication again — this time with the new provider.
How to Handle It¶
Your integration should always check the response structure after every API call in the 3DS flow, not just after the initial /api/transactions/authorize:
1. Check if `result` is present → Transaction is finalized (approved/declined)
2. Check if `action` is present → Handle fingerprint or challenge
3. Check if `redirect` is present → Redirect the customer
4. Check if `form_submit` is present → Auto-submit the form
Build a response handler, not a step counter
Rather than coding a linear sequence of steps, implement a single response handler that inspects each response and routes to the appropriate action. This naturally handles repeated authentication without special-case logic.
Limits¶
The gateway enforces limits at two levels:
- Per-authentication attempt: A maximum of one additional authentication round is allowed. If the issuer requests further authentication after this, the attempt will be automatically declined.
- Fallback routing: When a transaction is declined, the gateway may automatically retry with a different payment provider — up to N times (configurable, default is 5). Each retry goes through a full authorization and 3DS flow, so the customer may be asked to authenticate multiple times across retries.
After Authentication¶
Once the customer completes the 3DS challenge (via redirect or form submit flows), they will be redirected back to your return_url. Note that if the gateway performed an auto-retry with a different provider, the customer may need to complete 3DS again before reaching a final status. 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: THREE_DS_2_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
alt Transaction Finalized
Gateway -->> Server: Transaction result
Server -->> Browser: Payment result
else Additional Authentication Required
Gateway -->> Server: New action (THREE_DS_2_FINGERPRINT or THREE_DS_2_CHALLENGE)
Note over Server: Handle response same as initial authorize
Note over Browser: Repeat fingerprint or challenge flow
end
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