---
title: 3DS Integration Guide
slug: extensions/q35EZtu7au_9c_ogpkR8C
docTags: 
createdAt: 2025-12-14T14:23:41.918Z
---

# Integrating the 3DSecure in the Forter Cartridge

In order to integrate with 3DS recommendations, merchants will need to modify their **ForterOrder.ds** file to handle a new `"recommendation"` field in the response body as well as to pass additional 3DS authorization response data in the post-authorization request body.

## 1. Updates to API Schema

Once you've confirmed a 3DS integration with your dedicated Forter Implementations team, they will modify the schema to include additional fields in the request body `payment[0].creditCard` object and enable the exposure of a `"recommendations"` key in the response object returned by Forter in the pre-authorization call.&#x20;

Your Implementation team will direct you to the relevant API version and you can specify which version you should point to in the cartridge in the **cartridges > int\_forter > cartridge > scripts > init > forterServiceInit.js** file of your cartridge by modifying the default `Option .86` in `svc.addHeader("api-version", "Option .86");`

## 2. Enable Pre-Auth

Go to the `CheckoutServices.js` file in your Forter Cartridge. The exact path to this file is:
**Cartridges** > **int\_forter\_sfra** > **cartridge** > **controllers** > **CheckoutService.js**.

Open this file and **uncomment** the following code *AFTER* the `var order = COHelpers.createOrder(currentBasket);` function and *BEFORE* the `var handlePaymentResult = COHelpers.handlePayments(order, order.orderNo);` function in the file.

:::ExpandableHeading
### Full CheckoutServices.js file with Pre-auth

```javascript
'use strict';

var server = require('server');
server.extend(module.superModule);

var COHelpers      = require('*/cartridge/scripts/checkout/checkoutHelpers');
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
var ForterConfig   = require('int_forter_mfra/cartridge/scripts/lib/forter/ForterConfig.ds').ForterConfig;

server.replace('PlaceOrder', server.middleware.https, function (req, res, next) {
    var BasketMgr = require('dw/order/BasketMgr');
    var HookMgr = require('dw/system/HookMgr');
    var OrderMgr = require('dw/order/OrderMgr');
    var Resource = require('dw/web/Resource');
    var Transaction = require('dw/system/Transaction');
    var URLUtils = require('dw/web/URLUtils');
    var basketCalculationHelpers = require('*/cartridge/scripts/helpers/basketCalculationHelpers');

    var currentBasket = BasketMgr.getCurrentBasket();

    if (!currentBasket) {
        res.json({
            error: true,
            cartError: true,
            fieldErrors: [],
            serverErrors: [],
            redirectUrl: URLUtils.url('Cart-Show').toString()
        });
        return next();
    }

    if (req.session.privacyCache.get('fraudDetectionStatus')) {
        res.json({
            error: true,
            cartError: true,
            redirectUrl: URLUtils.url('Error-ErrorCode', 'err', '01').toString(),
            errorMessage: Resource.msg('error.technical', 'checkout', null)
        });

        return next();
    }

    var validationBasketStatus = HookMgr.callHook(
        'app.validate.basket',
        'validateBasket',
        currentBasket,
        false
    );

    if (validationBasketStatus.error) {
        res.json({
            error: true,
            errorMessage: validationBasketStatus.message
        });
        return next();
    }

    // Check to make sure there is a shipping address
    if (currentBasket.defaultShipment.shippingAddress === null) {
        res.json({
            error: true,
            errorStage: {
                stage: 'shipping',
                step: 'address'
            },
            errorMessage: Resource.msg('error.no.shipping.address', 'checkout', null)
        });
        return next();
    }

    // Check to make sure billing address exists
    if (!currentBasket.billingAddress) {
        res.json({
            error: true,
            errorStage: {
                stage: 'payment',
                step: 'billingAddress'
            },
            errorMessage: Resource.msg('error.no.billing.address', 'checkout', null)
        });
        return next();
    }

    // Calculate the basket
    Transaction.wrap(function () {
        basketCalculationHelpers.calculateTotals(currentBasket);
    });

    // Re-validates existing payment instruments
    var validPayment = COHelpers.validatePayment(req, currentBasket);
    if (validPayment.error) {
        res.json({
            error: true,
            errorStage: {
                stage: 'payment',
                step: 'paymentInstrument'
            },
            errorMessage: Resource.msg('error.payment.not.valid', 'checkout', null)
        });
        return next();
    }

    // Re-calculate the payments.
    var calculatedPaymentTransactionTotal = COHelpers.calculatePaymentTransaction(currentBasket);
    if (calculatedPaymentTransactionTotal.error) {
        res.json({
            error: true,
            errorMessage: Resource.msg('error.technical', 'checkout', null)
        });
        return next();
    }

    // Creates a new order.
    var order = COHelpers.createOrder(currentBasket);
    if (!order) {
        res.json({
            error: true,
            errorMessage: Resource.msg('error.technical', 'checkout', null)
        });
        return next();
    }
    // add or uncomment the new pre-auth code here:
    var argOrderValidate = {
                Order: order,
                orderValidateAttemptInput: 1
            },
            forterController = require('int_forter/cartridge/controllers/ForterValidate'),
            forterDecision   = forterController.ValidateOrder(argOrderValidate);
        // in case if no response from Forter, try to call one more time
        if (forterDecision.result === false && forterDecision.orderValidateAttemptInput == 2) {
            var argOrderValidate = {
                    Order: order,
                    orderValidateAttemptInput: 2
                },
                forterController = require('int_forter/cartridge/controllers/ForterValidate'),
                forterDecision   = forterController.ValidateOrder(argOrderValidate);
        }
        
        if (forterDecision.JsonResponseOutput.processorAction == 'void') {
            if (!empty(forterDecision.PlaceOrderError)) {
                return {error : true, forterErrorCode : forterDecision.PlaceOrderError};
            } else {
                return {error : true};
            }
        }  
        // end of pre-auth code

    var handlePaymentResult = COHelpers.handlePayments(order, order.orderNo);     // Handles payment authorization
    if (handlePaymentResult.error) {
        if (ForterConfig.forterShowDeclinedPage == true && !empty(ForterConfig.forterCustomDeclineMessage)) {
            res.json({
                error: true,
                errorMessage: ForterConfig.forterCustomDeclineMessage
            });
        } else {
            res.json({
                error: true,
                errorMessage: Resource.msg('error.technical', 'checkout', null)
            });
        }
        return next();
    }
    
    var fraudDetectionStatus = HookMgr.callHook('app.fraud.detection', 'fraudDetection', currentBasket);
    if (fraudDetectionStatus.status === 'fail') {
        Transaction.wrap(function () { OrderMgr.failOrder(order); });

        // fraud detection failed
        req.session.privacyCache.set('fraudDetectionStatus', true);

        res.json({
            error: true,
            cartError: true,
            redirectUrl: URLUtils.url('Error-ErrorCode', 'err', fraudDetectionStatus.errorCode).toString(),
            errorMessage: Resource.msg('error.technical', 'checkout', null)
        });

        return next();
    }

    // Places the order
    var placeOrderResult = COHelpers.placeOrder(order, fraudDetectionStatus);
    if (placeOrderResult.error) {
        res.json({
            error: true,
            errorMessage: Resource.msg('error.technical', 'checkout', null)
        });
        return next();
    }

    COHelpers.sendConfirmationEmail(order, req.locale.id);

    // Reset usingMultiShip after successful Order placement
    req.session.privacyCache.set('usingMultiShipping', false);

    // TODO: Exposing a direct route to an Order, without at least encoding the orderID
    //  is a serious PII violation.  It enables looking up every customers orders, one at a
    //  time.
    res.json({
        error: false,
        orderID: order.orderNo,
        orderToken: order.orderToken,
        continueUrl: URLUtils.url('Order-Confirm').toString()
    });

    return next();
});


module.exports = server.exports();
```

***
:::

## 3. Update your Business Manager

The additional code that you have added above to your **CheckoutServices.js** file allows your storefront to send order information to the Forter endpoint **BEFORE an authorization call is made to your payment gateway**.

You will also need to **update your Forter Business Manager extension** to show a customized error message (in cases of decline) for this updated Pre-Authorization Flow.

## 4. Handle Recommendations

You will need to add logic in your Forter SFCC cartridge to handle the new, `"recommendation"` field that will be included in the Forter response for orders that require 3DS.&#x20;

Merchants should make sure to include logic to handle all possible iterations of both the decisions and recommendations within their Forter cartridge (**ForterValidate.js**) and within their gateway cartridge to ensure that orders are correctly routed after receiving a pre-authorization decision and recommendation from Forter.

- In the case of **3DS for regulation**, the Forter pre-Authorization decision ("action") will be an `"approve"` along with a populated `"recommendations"` field.
- In the case of **3DS for risk assessment**, the Forter pre-Authorization decision ("action") will be a `"decline"` along with a populated `"recommendations"` field

In your SFCC cartridge, go to the `forterValidate` file, **cartridges/int\_forter\_sfra/cartridge/scripts/pipelets/forter/forterValidate.js** and add logic to handle both the decision and recommendation in cases where the recommendation field is populated with a value other than an empty string ("").

Possible Recommendation values are:

- `"VERIFICATION_REQUIRED_3DS_CHALLENGE"`
- `""` (when Forter does NOT recommend any 3DS or Exemption due to clear Approve)
- `"REQUEST_SCA_EXCLUSION_MOTO"`
- `"REQUEST_SCA_EXCLUSION_PREPAID"`
- `"RECOMMENDATION_REQUEST_SCA_EXCLUSION_ONE_LEG_OUT"`
- `"REQUEST_SCA_EXCLUSION_ONE_LEG_OUT"`
- `"REQUEST_SCA_EXCLUSION_MIT"`
- `"REQUEST_SCA_EXEMPTION_TRA"`
- `"RECOMMENDATION_REQUEST_SCA_EXEMPTION_LOW_VALUE"`
- `"REQUEST_SCA_EXEMPTION_LOW_VALUE"`
- `"REQUEST_SCA_EXEMPTION_CORP"`
- `"RECOMMENDATION_REQUEST_SCA_EXEMPTION_TRUSTED_BENEFICIARY"`
- `"REQUEST_SCA_EXEMPTION_TRUSTED_BENEFICIARY"`

The logic should be modified within the `validateOrder(args)` function around line 137 where the current options and logic for handling decisions and response body fields is located:

:::ExpandableHeading
### Example Modification Validate Order(args)

```javascript
    if (fResponse.actionEnum === 'DECLINED' && fResponse.recommendation[0] === undefined) { // modify accordingly
        if (sitePrefs.forterCancelOrderOnDecline === true) {
            fResponse.processorAction = 'void';
            if (sitePrefs.forterShowDeclinedPage === true && sitePrefs.forterCustomDeclineMessage) {
                resp.PlaceOrderError = {
                    code: sitePrefs.forterCustomDeclineMessage
                };
            }
        } else {
            fResponse.processorAction = 'skipCapture';
        }
    } else if (fResponse.actionEnum === 'NOT_REVIEWED') {
        fResponse.processorAction = 'notReviewed';
    } else if (fResponse.actionEnum === 'APPROVED' && fResponse.recommendation[0] == "") { // add logic to handle recommendation field when no rec is given
        fResponse.processorAction = sitePrefs.forterAutoInvoiceOnApprove ? 'capture' : 'skipCapture';
    } else if (fResponse.actionEnum === 'APPROVED' && fResponse.recommendation[0] != "REQUEST_SCA_EXEMPTION_LOW_VALUE") { //logic to handle/store 3DS rec for exemption
    fResponse.processorAction = sitePrefs.initiate3DSChallenge //were initiate3DSChallenge is a custom setting added to your dw site 'dw/system/Site'
    } else if (fResponse.actionEnum === 'DECLINED' && fResponse.recommendation[0] != "VERIFICATION_REQUIRED_3DS_CHALLENGE") { //logic to handle/store 3DS recommendation 
    fResponse.processorAction = sitePrefs.initiate3DSChallenge //were initiate3DSChallenge is a custom setting added to your dw site 'dw/system/Site'
    }
```
:::

## 5. Code Modifications to payment Gateway Cartridge

As seen in the above example, you'll need to modify your SFCC site preferences **dw > system > Site** to handle the new `"recommendations"` field in the response body. In the example above, the fucntionality is called `initiate3DSChallenge`. This functionality should allow your SFCC platform to accept a recommendation and trigger a 3DS challenge from your payment gateway.

Each gateway has different 3DS challenge requirements and configurations. It will be incumbant on each merchant to handle the integration of the recommendation into their gateway and configure the relevant 3DS challenge to a customer based on the recommendation provided by Forter.

## 6. Post-Auth Options

For a second, synchronous post-auth decision, you will need to update the mapping in your ForterOrder.ds file to include the authorization and 3DS response data from your gateway and make sure your ForterValidate.js file also handles the second post-auth Forter decision.&#x20;

For an post-auth order status call *without* a new Forter decision ("action"), you'll modify the **ForterUpdate.js** job and authorization response mapping in the corresponding `ForterPayment()` function.&#x20;

### Option 1: Post-Auth Validation Call

After the gateway has successfully initiated 3DS and authorized payment (or rejected payment), the results should be included in the post-authorization call to the Forter cartridge. These fields should be added to the `ForterCreditCard(auth, cc)` function mapping of your **ForterOrder.ds** file. This flow is applicable for merchants who may not be receiving a Forter payment recommendation (3DS or PSD2) but want to provide Forter's models with the payments outcome from their processor.

:::ExpandableHeading
### Example Modified ForterCreditCard() with 3DS results

```javascript
function ForterCreditCard(auth, cc) {
    var processorResponseCode = '';
    var processorResponseText = '';
    var creditCardExpMonth;

    // format the expiration month. from 1 to 01, etc.
    if (cc.creditCardExpirationMonth.toString().length === 1) {
        creditCardExpMonth = '0' + cc.creditCardExpirationMonth.toString();
    } else {
        creditCardExpMonth = cc.creditCardExpirationMonth.toString();
    }

    this.nameOnCard = cc.creditCardHolder;
    this.cardBrand = cc.creditCardType;

    if (cc.creditCardNumber.substring(0, 6).indexOf('*') > -1) {
        this.bin = session.forms.billing.creditCardFields.cardNumber.value.substring(0, 6); // session.forms.billing.paymentMethods.creditCard.number.value.substring(0, 6);
    } else {
        this.bin = cc.creditCardNumber.substring(0, 6);
    }

    this.lastFourDigits = cc.creditCardNumberLastDigits;
    this.expirationMonth = creditCardExpMonth;
    this.expirationYear = cc.creditCardExpirationYear.toString();

    this.verificationResults = {};
    this.verificationResults.avsFullResult = auth ? auth.avsResultCode.toString() : '';  
    this.verificationResults.cvvResult = auth ? auth.cvvResultCode.toString() : ''; 
    this.verificationResults.authorizationCode = auth ? auth.authCode.toString() : '';

    // ******************************** new 3DS response data ********************************    
    this.verificationResultsauthorizationPolicy =  "3DSecure", //can be hardcoded
    this.verificationResults.cavvResult = auth.body ? replyMessage.decisionEarlyReply.rcode,
    this.verificationResults.eciValue auth.body ? auth.payerAuthValidationReply.eciRaw,
    this.verificationResults.threeDsStatus = auth.payerAuthValidationReply.authenticationMessage || '',
    this.verificationResults.threeDsStatusCode = auth.payerAuthValidationReply.authenticationStatusCodeRaw || '',
    this.verificationResults.threeDsStatusReasonCode = auth.body.status,
    this.liabilityShift = auth.payerAuthValidationReply ? true : false, //should be a boolean value
    threeDsVersion = auth.payerAuthValidationRepl.auth.version
    this.verificationResults.threeDsInteractionMode = "FRICTIONLESS", // modify according to 3ds used
    this.verificationResults.threeDsChallengeCancelCode = replyMessage.decisionEarlyReply.challengeCodeNumeric || '',
    this.verificationResults.authenticationChallengeStartTime = new Number((order.header.Timestamp/1000).toFixed()); , // should be timestamp as int in unix format
    this.verificationResults.authenticationTriggered =  auth.payerAuthValidationReply ? true : false, //should be a boolean value
    this.verificationResults.external3dsVendorPayload =  {}, // optional
    this.verificationResults.external3dsVendorPayloadBeforeChallenge =  {}, // optional 
    this.verificationResults.authenticationMethodType = "THREE_DS", // can be hardcoded if this is the only auth method
    this.verificationResults.authorizationProcessedWith3DS =  auth.payerAuthValidationReply ? true : false, //should be a boolean value
    this.verificationResults.partyExecuted3DS =  "PSP" // add relevant vendor(s)
    // **************************** end of new 3DS response data ***************************
    },
    this.paymentGatewayData = {};
    this.paymentGatewayData.gatewayName = 'AUTHORIZE.NET';                   
    this.paymentGatewayData.gatewayTransactionId = auth ? auth.transId.toString() : '';  

    if (auth && auth.errors.errorCode.length > 0) {                            
        processorResponseCode = auth.errors.errorCode.toString();      
        processorResponseText = auth.errors.errorText.toString();      
    } else if (auth && auth.messages.code.length > 0) {                        
        processorResponseCode = auth.messages.code.toString();         
        processorResponseText = auth.messages.description.toString();  
    }

    this.verificationResults.processorResponseCode = processorResponseCode;
    this.verificationResults.processorResponseText = processorResponseText;
}
```
:::

### Option 2: Post-Auth Order Status Call

**Step 1: Include Post-Auth Status Update script**&#x20;
The example Authorize.net script below outlines how to update the script that handles credit card processing so that it includes the relevant post-authorization and 3DS order status call to Forter. Note that the two default order statuses are:

- **"PROCESSING"** (when the gateway has authorized the payment)
- **"CANCELED\_BY\_MERCHANT"** (when the gateway/processor has NOT authorized payment)

The code example below applies to all SFRA and SG pre-auth/status implementations and the full `postAuthOrderStatusUpdate()` function can be found in forterValidate.js file (**cartridges** > **int\_forter\_sfra** > **cartridge** > **scripts** > **pipelets** > **forter** > **forterValidate.js**)

```javascript

    var argOrderUpdate = { 
        orderNumber: orderNumber,
        updateAttempt: 1 
    },
    forterCall = require('*/cartridge/scripts/pipelets/forter/forterValidate'),
    forterDecision = forterCall.postAuthOrderStatusUpdate(argOrderUpdate, "PROCESSING");

    if (forterDecision.result === false && forterDecision.updateAttempt == Option ) { 
        forterDecision.updateAttempt = Option ; 
        forterCall.postAuthOrderStatusUpdate(argOrderUpdate, "PROCESSING");
    }

```

**Step 2: Customize ForterUpdate.js file**&#x20;
After uncommenting the pre-auth code `CheckoutServices.js` file, customize the `forterUpdate.js` file to include your gateway's authorization AND 3DS response in the status call to Forter. The exact path to the `forterUpdate.js` file is **cartridges** > **int\_forter\_sfra** > **cartridge** > **scripts** > **lib** > **forter** > **dto** > **forterUpdate.js**). Customize the `ForterPayment(payment, order)` function to map your specific gateway authorization response.

:::ExpandableHeading
### Example ForterCreditCard() auth function

````text
```javascript

function ForterCreditCard(auth, cc) {
    var processorResponseCode = '';
    var processorResponseText = '';
    var creditCardExpMonth;

    // format the expiration month. from 1 to 01, etc.
    if (cc.creditCardExpirationMonth.toString().length === 1) {
        creditCardExpMonth = '0' + cc.creditCardExpirationMonth.toString();
    } else {
        creditCardExpMonth = cc.creditCardExpirationMonth.toString();
    }

    this.nameOnCard = cc.creditCardHolder;
    this.cardBrand = cc.creditCardType;

    if (cc.creditCardNumber.substring(0, 6).indexOf('*') > -1) {
        this.bin = session.forms.billing.creditCardFields.cardNumber.value.substring(0, 6); // session.forms.billing.paymentMethods.creditCard.number.value.substring(0, 6);
    } else {
        this.bin = cc.creditCardNumber.substring(0, 6);
    }

    this.lastFourDigits = cc.creditCardNumberLastDigits;
    this.expirationMonth = creditCardExpMonth;
    this.expirationYear = cc.creditCardExpirationYear.toString();

    this.verificationResults = {};
// ******************************** new 3DS response data ********************************    
    this.verificationResultsauthorizationPolicy =  "3DSecure", //can be hardcoded
    this.verificationResults.cavvResult = auth.body ? replyMessage.decisionEarlyReply.rcode,
    this.verificationResults.eciValue auth.body ? auth.payerAuthValidationReply.eciRaw,
    this.threeDsStatus = auth.payerAuthValidationReply.authenticationMessage || '',
    this.threeDsStatusCode = auth.payerAuthValidationReply.authenticationStatusCodeRaw || '',
    this.threeDsStatusReasonCode = auth.body.status,
    this.liabilityShift = auth.payerAuthValidationReply ? true : false, //should be a boolean value
    this.threeDsVersion = auth.payerAuthValidationRepl.auth.version
    this.threeDsInteractionMode = "FRICTIONLESS", // modify according to 3ds used
    this.threeDsChallengeCancelCode = replyMessage.decisionEarlyReply.challengeCodeNumeric || '',
    this.authenticationChallengeStartTime = new Number((order.header.Timestamp/1000).toFixed()); , // should be timestamp as int in unix format
    this.authenticationTriggered =  auth.payerAuthValidationReply ? true : false, //should be a boolean value
    this.external3dsVendorPayload =  {}, // optional
    this.external3dsVendorPayloadBeforeChallenge =  {}, // optional 
    this.authenticationMethodType = "THREE_DS", // can be hardcoded if this is the only auth method
    this.authorizationProcessedWith3DS =  auth.payerAuthValidationReply ? true : false, //should be a boolean value
    this.partyExecuted3DS =  "PSP"

    this.verificationResults.avsFullResult = auth ? auth.avsResultCode.toString() : '';    
    this.verificationResults.cvvResult = auth ? auth.cvvResultCode.toString() : ''; 
    this.verificationResults.authorizationCode = auth ? auth.authCode.toString() : '';    
    this.paymentGatewayData = {};
    this.paymentGatewayData.gatewayName = 'AUTHORIZE.NET';                
    this.paymentGatewayData.gatewayTransactionId = auth ? auth.transId.toString() : '';  

    if (auth && auth.errors.errorCode.length > 0) {                            
        processorResponseCode = auth.errors.errorCode.toString();      
        processorResponseText = auth.errors.errorText.toString();      
    } else if (auth && auth.messages.code.length > 0) {                        
        processorResponseCode = auth.messages.code.toString();         
        processorResponseText = auth.messages.description.toString();  
    }

    this.verificationResults.processorResponseCode = processorResponseCode;
    this.verificationResults.processorResponseText = processorResponseText;
    }   
};
````
:::
