Using 3DS to run transactions

Background
Before starting this tutorial, make sure you understand the following topics from the Quick start section:
  • Sandbox environments (especially about testing).
  • Authentication (about authenticating using your API user and when you need a login versus a one-time-use token).
  • 3DS requirements for important overview information and concepts.
  • 3D Secure in the Getting Started section, for a general overview of the product.

The configuration and payment flow with 3DS is similar to a regular payment flow, but then has a separate set of steps where the customer must authenticate with their bank. This tutorial provides guidance on the differences and how to ensure your system is working for 3DS in the way you expect. (For information about running transactions without 3DS, see Creating a card checkout page with the iframe, Creating a card checkout page with your own form, and Running a card transaction with the API).

📘

Note

In order to effectively process and manage 3DS transactions, you need to be aware of the following:

  1. Nexio strongly recommends that you include an order number with each transaction. Ideally, this value should be unique to the merchant. If you run 3DS transactions and you do not use a unique order number, there will be payment flows that you won't be able to reconcile.
    Nexio recommends a format for the order number of a unique value that also includes an attempt number so that you can track attempts per order, such as when a payment attempt fails. For example, something like [order_number]-[attempt_number].
  2. Nexio highly recommends that you configure and use webhooks. For more information about doing this, see Webhooks.
  3. Nexio suggests that you configure listeners on the merchant website for receiving events. For more information about how this works, see steps 2 and 6 in the Creating a card checkout page with the iframe tutorial.

For 3DS, the payment flow is as follows:

  • If you need to save a payment card:
    • Get a one-time-use token, making sure that 3DS is enabled.
    • Use that one-time-use token to save the payment card.
    • Save the card token for later use.
  • Run the transaction using a saved card token or full card information.
  • Put the 3DS redirect link as the SRC for an iframe or an HTML element such as a link. Save the asyncTraceId to use for tracking the transaction and for troubleshooting.
  • Depending on verification needs, the customer may either get redirected to their bank's 3DS page where they can authenticate or the redirect happens behind the scenes and the bank automatically authenticates the customer.
  • The 3DS process completes and returns control to the merchant site, where you can display a payment confirmation page.

Getting the one-time-use token

If you need to save a payment card for the customer, you must first request a one-time-use token. The Integrations Support team configures your merchant account by default to be enabled for 3DS transactions, so you should not need to pass the parameter. We do it in these steps for clarity.

You also need to get a one-time use token if you want to use the Nexio iframe to run the transaction.

For information about values saved in the one-time-use and card tokens, see the What data is saved with tokens? topic.

To get the one-time-use token for a 3DS payment flow, do the following:

  1. If you want to use the Nexio iframe: Complete steps 1-2 in the Creating a save card page with the iframe tutorial.
    If you want to use your own form: Complete steps 1-2 in the Creating a save card page with your own form tutorial.

  2. Use your backend server to send a POST request to the ecommerce Create one-time-use token endpoint.
    Include any parameters that you want to save to the one-time-use or saved card token. Any iframe uiOptions or processingOptions must be included in this request. Nexio strongly recommends that you also send the appropriate paymentType value as part of the request. For more information, see Payment type (paymentType) in Constant transaction values.

       curl --request POST \
            --url https://api.nexiopaysandbox.com/pay/v3/token \
            --header 'accept: application/json' \
            --header 'authorization: Basic [Base64_encoded_login]' \
            --header 'content-type: application/json' \
            --data '
       {
         "data": {
           "currency": "EUR",
           "customer": {
             "customerRef": "[unique_customer_ref]",
             "firstName": "[customer_first_name]",
             "lastName": "[customer_last_name]",
             "billToAddressOne": "[customer_address1]",
             "billToAddressTwo": "[customer_address2]",
             "billToCity": "[customer_city]",
             "billToState": "[customer_state]",
             "billToPostal": "[customer_postal]",
             "billToCountry": "[customer_country]",
           }
         },
         "processingOptions": {
           "paymentType": "initialUnscheduled",
           "check3ds": true
         },
         "uiOptions": {
           "displaySubmitButton": true,
           "hideBilling": {
             "hidePhone": true
           },
           "requireCvc": true,
           "hideCvc": false
         }
       }
    '
    

    A successful request returns the following type of response:

    {  
      "expiration": "2023-09-30T18:26:03.000Z",  
      "token": "830d36f6-a5e3-4455-9600-3a55b63e2fc2",  
      "fraudUrl": "<https://api.nexiopaysandbox.com/pay/v3/fingerprint?token=01080f80-76b8-4363-845d-67e8623bf170">  
    }
    

    You will use the token in the next set of steps. So, pass it from the backend to your frontend webpage.

  3. If you next need to save a card token, go to the following set of steps.
    If you already have a saved card token and need to run the 3DS transaction, go to the "Running a transaction with 3DS" set of steps below.

Saving the card token

After you have the one-time-use token and if you next need to save a card token for a customer, do the following:

  1. If you are using the Nexio iframe: Complete steps 4-6 in the Creating a save card page with the iframe tutorial to load the iframe for saving the card token and adding event listeners.

    If you want to use your own form: Complete steps 4-9 in the Creating a save card page with your own form tutorial for saving the card token and adding event listeners.

    Your frontend webpage can use a script to load the iframe, or you could provide your own form and send the one-time-use token you got from the previous set of steps to the Save card token endpoint yourself.

    <html lang="en">
    <body>
    <style>
        html {
            font-family: "arial",serif;
        }
    
        #iframe {
            height: 1500px;
            border: 0;
        }
    </style>
    
    <script>
      function loadIframe(otu) {
            let iframe = document.getElementById('iframe');
    
            iframe.src = 'https://api.nexiopaysandbox.com/pay/v3/saveCard?token=' + otu;
      }
      
      // Other code here for requesting the one-time-use token when the page loads and getting it from the backend.
      
      window.addEventListener('message', (message) => {
            if (message.data.event === 'loaded') {
                console.log('iframe successfully loaded')
    
            }
            if (message.data.event === 'cardSaved') {
                console.log(message.data.data.token.token);
                fetch('http://localhost:3000/saveToken', {
                    method: 'POST',
                    body: JSON.stringify({ cardToken: message.data.data.token.token} ),
                    headers: {
                        'Content-Type': 'application/json'
                    }
                })
                alert("Card saved successfully!");
            }
        });
      </script>
      
      <div>
        <h3>Save Credit/Debit Card</h3>
        <p>Provide the card and address information that you want to save for future purchases.</p>
        <iframe id="iframe" src=""></iframe>
      </div>
    </body>
    </html>  
    

  2. After the customer provides their card information and submits the details in the iframe or through your own form, a successful response to your backend server looks similar to the following:

    {
      "token": {
        "token": "[unique_token]",
        "firstSix": "479300",
        "lastFour": "3313",
        "cardType": "visa"
      },
      "card": {
        "cardType": "visa",
        "cardHolderName": "John H Doe",
        "expirationMonth": "12",
        "expirationYear": "28"
      },
      "cardType": "visa",
      "data": {
        "customer": {
          "customerRef": "[unique_customer_ref]",
          "firstName": "John",
          "lastName": "Doe",
          "billToAddressOne": "2147 West Silverlake Drive",
          "billToAddressTwo": "Apt 42",
          "billToCity": "Scranton",
          "billToState": "PA",
          "billToPostal": "18503",
          "billToCountry": "US"
        }
      },
      "merchantId": "[merchant_id]"
    }
    
  3. The saved card token is in token.token. You may want to save or store this value on your backend server or database, associated with the user. You can use that saved card token when the customer requests to pay for a transaction.

Running a transaction with 3DS

The next part of the payment process is all about sending a payment request. For 3DS, the customer needs to also be authenticated with their bank.

You can run the transaction with either a saved card token (see the previous sets of steps) or the full card information (if your gateway connection allows that).

To run the transaction, do the following:

  1. If you are using the Nexio payment iframe: Complete steps 1-4 in the Creating a card checkout page with the iframe tutorial. If you have not used it to save a card token already, you can use the one-time-use token from the set of steps above to load this payment iframe.
    If you want to use your own payment form: Complete steps 1-3 in the Creating a card checkout page with your own form tutorial.

  2. If you are using the Nexio payment iframe: Use an unused one-time-use token to load the iframe. Set the start of the iframe's URL to the Run card transaction with iframe endpoint (https://api.nexiopaysandbox.com/pay/v3). Append the one-time-use token to the iframe's URL in a query parameter called token. Assign the result to your iframe's src tag.

    var iframeBaseUrl = "https://api.nexiopaysandbox.com/pay/v3";
    var oneTimeUseToken = "?token=" + token;
    var returnHtml = "&shouldReturnHtml=true";
    var url = iframeBaseUrl + oneTimeUseToken + returnHtml;
    window.document.getElementById('myIframe').src = url;
    
    <iframe id="myIframe" src=""></iframe>
    

    If you are using your own payment form: Configure your frontend webpage to send the transaction details and payment method information to your backend server.

    <div class="row" id="paymentDiv" name="paymentDiv">
    <form id="myForm" action="javascript:;" method="post" onsubmit="runCardTransaction(this);">
      [form fields go here]
      <input type="submit" value="Pay" class="btn" id="submitButton">
    </form>
    </div>
    <div id="onRedirect" name="onRedirect" style="display:none;">
    <iframe id="3ds-validation" src=""></iframe>
    </div>
    
    let currentAsyncTraceId;
    
    function runCardTransaction() {
            let amount = document.forms["myForm"]["total"].value;        
            let cart = document.forms["myForm"]["cart"].value;
            let orderNumber = document.forms["myForm"]["orderNumber"].value;
            let cname = document.forms["myForm"]["cname"].value;
            let ccnum = document.forms["myForm"]["ccnum"].value;
            let cardtype = document.forms["myForm"]["cardtype"].value;
            let billingStreet = document.forms["myForm"]["address"].value;
            let billingStreet2 = document.forms["myForm"]["address2"].value;
            let billingCity = document.forms["myForm"]["city"].value;
            let billingState = document.forms["myForm"]["state"].value;
            let billingZip = document.forms["myForm"]["zip"].value;
            let billingCountry = document.forms["myForm"]["country"].value;
            let shippingStreet = document.forms["myForm"]["shipaddress"].value;
            let shippingStreet2 = document.forms["myForm"]["shipaddress2"].value;
            let shippingCity = document.forms["myForm"]["shipcity"].value;
            let shippingState = document.forms["myForm"]["shipstate"].value;
            let shippingZip = document.forms["myForm"]["shipzip"].value;
            let shippingCountry = document.forms["myForm"]["shipcountry"].value;
            let customerEmail = document.forms["myForm"]["email"].value;
    
           formData = {
                amount,
                cart,
                orderNumber,
                cname,
                ccnum,
                cardtype,
                billingStreet,
                billingStreet2,
                billingCity,
                billingState,
                billingZip,
                billingCountry,
                shippingStreet,
                shippingStreet2,
                shippingCity,
                shippingState,
                shippingZip,
                shippingCountry,
                customerEmail
            }
    
            document.getElementById("paymentDiv").style.display = "none"; 
            document.getElementById("onRedirect").style.display = "block";
            document.getElementById("3ds-validation").style.display = "block";
    
            fetch('http://localhost:3000/pay', {
                method: 'POST',
                body: JSON.stringify(formData),
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(function (response) {
                return response.json();
            }).then((objData) => {
                if (objData.status === "redirect") {
                    console.log('------->response Data for 3DS redirect', objData);
                    currentAsyncTraceId = objData.asyncTraceId
                    document.forms["redirectForm"]["redirLink"].setAttribute('value', objData.redirectUrl);
                    load3dsIframe(objData.redirectUrl);
                }
                else if (objData.error === 446) {
                    // this should only happen when 3DS has not been enabled for your account
                    alert("Problem with the merchant account.");
                }
                else if (objData.error === 409) {
                    // this should only happen when 3DS is not correctly configured for your account
                    alert("Problem with the merchant account. Routing rules issue.")
                }
                else {
                    // this only happens for non-3DS payment flow
                    alert("Payment success at function level! Now send to confirmation page.");
                    window.open("http://localhost/3ds/confirmation.html");
                }
                
            });
            return false;
        }
    
        function load3dsIframe(redirectUrl) {
            let iframe = document.getElementById('3ds-iframe');
            let regularLink = document.getElementById('redirectalink');
            iframe.src = redirectUrl;
            regularLink.href = redirectUrl;
        }
    
    import express from 'express';
    const app = express();
    import cors from 'cors';
    import fetch from 'node-fetch';
    import fs from 'fs';
    const port = 3000;
    const encodedLogin = '[your_api_encoded_login]';
    
    // variables go here
    [other variables here...]
    let asyncTraceId;
    
    // other code here for loading the server and settings and listening
    
    // get request from frontend website
    app.post('/pay', (req, res) => {
    	return runCardTransactionFunction(req.body).then((response) => {
        	console.log('------->run card response', response);
        	// check for a 3DS redirect and save the asyncTraceId to use for troubleshooting or tracking
          if (response.status === 'redirect') {
                asyncTraceId = response.asyncTraceId;
                console.log('------->asyncTraceId for troubleshooting', asyncTraceId);
            }
            return res.json(response);
        })
    });
    
    function runCardTransactionFunction(body) {
    	cartAmount = body.amount;
    	shoppingCart = body.cart;
    	orderNumber = body.orderNumber;
    	cardholderName = body.cname;
    	cardLastFour = body.ccnum;
    	cardType = body.cardType;
    	customerEmail = body.customerEmail;
    	customerStreet = body.billingStreet;
    	customerStreet2 = body.billingStreet2;
    	customerCity = body.billingCity;
    	customerState = body.billingState;
    	customerPostal = body.billingZip;
    	customerCountry = body.billingCountry;
    	customerShipStreet = body.shippingStreet;
    	customerShipStreet2 = body.shippingStreet2;
    	customerShipCity = body.shippingCity;
    	customerShipState = body.shippingState;
    	customerShipPostal = body.shippingZip;
    	customerShipCountry = body.shippingCountry; 
    	// setting this to specifically run as 3DS. You may need to include code to handle non-3DS as well
      let runCardRequest3ds = {
    		"data": {
    			"amount": `${cartAmount}`,
    			"currency": "EUR",
            	"cart": `${shoppingCart}`,
            	"customer": {
            		"orderNumber": `${orderNumber}`,
            		"email": `${customerEmail}`,
            		"billToAddressOne": `${customerStreet}`,
            		"billToAddressTwo": `${customerStreet2}`,
            		"billToCity": `${customerCity}`,
            		"billToState": `${customerState}`,
            		"billToPostal": `${customerPostal}`,
            		"billToCountry": `${customerCountry}`,
            		"shipToAddressOne": `${customerShipStreet}`,
            		"shipToAddressTwo": `${customerShipStreet2}`,
            		"shipToCity": `${customerShipCity}`,
            		"shipToState": `${customerShipState}`,
            		"shipToPostal": `${customerShipPostal}`,
            		"shipToCountry": `${customerShipCountry}`
            	}
    		},
    		"tokenex": {
    			"token": `${cardToken}`
    		},
    		"processingOptions": {
    			"paymentType": "initialUnscheduled",
    			"checkFraud": false,
    			"check3ds": true
    		}
    	};
    
    	return fetch('https://api.nexiopaysandbox.com/pay/v3/process', {
            method: 'post',
            body: JSON.stringify(runCardRequest3ds),
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Basic ${encodedLogin}`
            }
        }).then((res) => {
            console.log('------->run card res', res);
            return res.json();
        })
    };
    
  3. Add any necessary event listeners to your JavaScript.

        window.addEventListener('message', (message) => {
            // for added troubleshooting during development.
            console.log('Listener message received: ', message.data.event);
            
            if (message.data.event === 'initiate') {
                console.log('Starting 3DS payment flow for asyncTraceId: ', currentAsyncTraceId);
    
            }
    
            if (message.data.event === 'processed') {
                console.log('User authenticated.');
                
                fetch('http://localhost:3000/savePaymentId', {
                    method: 'POST',
                    body: JSON.stringify({ paymentId: message.data.data.id} ),
                    headers: {
                        'Content-Type': 'application/json'
                    }
                });
                alert("Payment success! Now send to confirmation page");
                window.open("http://localhost/3ds/confirmation.html");
            }
    
            if (message.data.event === 'error') {
                if (message.data.data.error === 481) {
                    console.log('Transaction canceled by customer.');
                    alert("Transaction canceled. Please reload the page and try again or go back to saving a card token.");
                }
                else if (message.data.data.error === 435) {
                    console.log(message.data);
                    alert('Transaction was declined. Please reload the page and try again or go back to saving a card token.');
                }
                else {
                    console.log(message.data);
                    alert("There was a problem with the payment. Please reload the page and try again.");
                }
            }
        });
    

    This helps to catch any problems and makes sure you have the information you need for troubleshooting.

  4. Add in code that generates a unique order number for this transaction. For the payment iframe, you specify the value in the one-time-use token. For your own form, you can also include it in the payment request, as in the following example.

    function loadForm(objData) {
        // function for getting saved data about customer and order and payment methods and
        // prefilling the form
        var randomOrderNumber = generateRandomOrderNumber(12) + '-1';
        // use the value to prefill a form field
        document.forms["myForm"]["orderNumber"].setAttribute('value', randomOrderNumber);
    }
    function generateRandomOrderNumber(length) {
        var result = '';
        var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        var charactersLength = characters.length;
        for (var i = 0; i < length; i++) {
          result += characters.charAt(Math.floor(Math.random() * charactersLength));
        }
          return result;
    }
    

    Now you are ready for the customer to open the checkout page.

  5. After the page loads, the customer completes any required information about how to pay (selecting a saved card, for example) and clicks the payment button.

    If the transaction requires 3DS authentication: The response from the Run card transaction includes a redirectUrl. See the code examples above for how to handle this response. In addition, you should save the asyncTraceId from the response for potential tracking or troubleshooting purposes. For additional information about this, see the Troubleshooting asynchronous transactions tutorial.

    {
      merchantId: '[merchant_id]',
      status: 'redirect',
      message: 'Provide redirect url in iFrame to shopper to complete 3ds authentication and transaction',
      redirectUrl: 'https://api.nexiopaysandbox.com/pay/v3/threeDS/frame?token=[random_token]',
      asyncTraceId: '[random_token]',
      'random-[nnnn]': '[random_value]'
    }
    

    If the transaction does not require 3DS authentication: The response will be as normal, indicated in the Creating a card checkout page with the iframe step 7 or in the Creating a card checkout page with your own form step 5. Skip to step 6 below.

    📘

    Note

    You can replace the iframe part of the URL with popup to skip the loading of the iframe and direct the customer to the bank page. You would not use the Nexio iframe for this flow, and would use either a link or other method to open the popup for the customer. Note that if you use a link, you need to include a customerRedirectUrl in the payment flow. The following is some code that you might use to take the token and change the URL.

    <script>
        let currentAsyncTraceId;
    
        function runCardTransaction() {
          // see other code samples for what may go here
          fetch('http://localhost:3000/pay', {
            // see other code samples for this part
            }).then((objData) => {
              if (objData.status === "redirect") {
                // see other code samples for this part
                currentAsyncTraceId = objData.asyncTraceId;
                // change to popup for 'redirect' link
                let popupLink = "https://api.nexiopaysandbox.com/pay/v3/threeDS/popup?token=" + currentAsyncTraceId;
                document.forms["redirectForm"]["redirLink"].setAttribute('value', popupLink);
              }
          });
        }
    </script>
    

  6. If the transaction requires 3DS authentication and if you are using the 3DS iframe: Nexio’s iframe prompts shoppers to confirm they are being redirected for authentication.
    If the transaction requires 3DS authentication and if you are using your own form or other HTML element: You should provide some text or information to tell the customer that they need to be redirected to authenticate. The customer then clicks the link or button or image that you provide. As indicated in the note at the end of the previous step, you can also skip this and open the popup directly for your customers.
    Upon confirmation, a new tab opens. Then, depending on the bank's authentication requirements, the customer may need to complete authentication with their bank or the redirect and authentication may happen automatically without customer input.
    You can use the asyncTraceId from the previous response for potential tracking or troubleshooting purposes. For additional information about this, see the Troubleshooting asynchronous transactions tutorial.

  7. After customers successfully authenticate, the system attempts to run the transaction.

    Upon completion, the response gets returned as a message to the browser.

    {
      "amount": 34.25,
      "authCode": "035410",
      "card": {...},
      "currency": "USD",
      "data": {...},
      "gatewayResponse": {...},
      "id": "[paymentId]",
      "kountResponse": {...},
      "merchantId": "[merchant_id]",
      "token": {...},
      "transactionDate": "2023-01-15T13:19:39.329Z",
      "transactionStatus": "pending",
      "transactionType": "sale"
    }
    

    📘

    Notes

    • The response schema will be the same as that of a standard POST request to the Run card transaction endpoint.
    • The page does not generate a default confirmation page. We recommend using our response to create your own confirmation page. You may instead use the customerRedirectUrl parameter to control where to direct the customer after 3DS authentication (whether successful or not).
    • You can use the asyncTraceId from the redirect response for potential tracking or troubleshooting purposes. For additional information about this, see the Troubleshooting asynchronous transactions tutorial.