A Guide to Integrating with Adyen Web for 3D Secure 2 Payments

sarahcodes_dev

Sarah 🦄

Posted on January 12, 2024

A Guide to Integrating with Adyen Web for 3D Secure 2 Payments

The goal of this guide is to provide the necessary context and examples to empower developers to handle 3D Secure 2 payments with the Adyen Web solution for the browser. This guide has a companion demo application with code examples for each flow which can be found in our github examples.

What is 3D Secure 2?

3 Domain Secure (3DS) is a security measure for online payments. The 3 domains (acquirer, scheme, and issuer) interact with each other using a 3DS protocol where they exchange information, providing an authentication mechanism for the consumer during a transaction.

3D Secure helps prevent fraud and is available for Card Not Present (CNP) transactions with all major card networks, and is mandatory in the EU, following the Revised Payment Services Directive (PSD2). For more in depth details about PSD2 and SCA you can refer to our documentation.

When facilitating a card payment online we want to do all we can to ensure the shopper is who they say they are. This way we reduce fraudulent transactions and improve authorisation rates for real shoppers.

There are two different ways customers can verify themselves using 3D Secure: frictionless and challenge. The frictionless flow is based on background information that doesn't require the customer to actively verify themselves. The challenge flow means the issuer has determined the transaction needs additional verification from the customer. For example; the shopper may have to enter a passcode they receive on their phone. The more information about the shopper we send in the payment request the higher the chances of a frictionless flow.

How to support 3DS2 transactions with Adyen

Our Adyen Web SDK and server libraries support two options for 3DS2 payments:

Native

The authentication experience occurs on your merchant page. Our SDK renders the necessary components and communicates directly with the ACS. The shopper never leaves your page.

Redirect

The authentication experience occurs on an Adyen page. Our SDK will redirect the shopper to an Adyen domain. The shopper is then redirected back to your merchant page once the authentication is complete.

Both Native and Redirect flows can be either frictionless or challenged. Which option to choose depends on a multitude of factors and is out of scope for this guide. For more in depth information about each flow you can refer to our documentation. This guide will show you how to integrate for both flows.

Comparing Adyen Solutions

When choosing a solution with Adyen you have a couple of choices to make; on the client side you can choose to integrate with our Drop-in or our Components. On the server side you can choose to integrate with our Sessions flow or our Advanced flow. This tutorial will demonstrate the Drop-in, you can follow along with our Github example.

Drop-in vs. Components

Drop-in is our pre-built UI for accepting payments. We recommend Drop-in as it renders a full list of available payment methods and does a lot of the heavy lifting for you. It also has 3D Secure 2 support built-in.

Components are our customizable UI components which render individual payment methods. Choose Components if you prefer to compose your own UI.

Both Drop-in and Components are available from our Adyen Web SDK.

Sessions flow vs. Advanced flow

The Sessions flow consists of one API call that creates a payment session which is used by the Adyen Web SDK to facilitate the payment flow. The advantage is that with the Sessions flow you don’t have to do any extra work for 3DS2, everything is handled by the Drop-in/components. However, it’s important to note that the Sessions flow does not support 3DS2 redirect flow so if you wish to use redirect then you need to use the Advanced flow.

The Advanced flow consists of three API calls: /paymentMethods, /payments and /payments/details. You will need to configure and support each of these API calls in the advanced flow to facilitate the payment. Luckily you can use one of our Adyen server libraries to make this a lot easier.

When integrating with Adyen you can break it down to three core parts; the server, the client and the webhook.

Server: Handle the payment request(s) on your server using one of our API flows.
Client: Show the payment on your web page using our pre-built Drop-in or composing your own UI with our Components.
Webhook: Configure and receive webhook notifications with the outcome of each payment. Webhooks are out of scope for this guide.

How 3DS2 is applied is dependent on your Dynamic 3DS rules. For more information on this please refer to our documentation.

Integrate with Sessions Flow

This guide assumes you already have your Adyen API key and client key and are pointing to our TEST environment. If you haven’t you can find directions here.

Let’s start with configuring the Sessions flow on our backend. The Sessions flow consists of one API call /sessions.

Install Adyen API library

The first step is to install the Adyen API library, these examples will be in TypeScript so we’re going to use the Node.js library. These examples are built using version 15.1.0 of the api-library which utilizes the latest version of Checkout API (71).

If you have npm installed, you install the library in your app by running:

npm install --save @adyen/api-library
npm update @adyen/api-library

Enter fullscreen mode Exit fullscreen mode

Configure CheckoutAPI

Now we have the library installed, let’s write our payment service. The first step is to initialize the Client object with your API key and environment, then we need to pass this client into the CheckoutAPI constructor to initialize our CheckoutAPI object.

// ../backend/../payments.ts

import { Client, CheckoutAPI } from '@adyen/api-library';

// initialise the client object
 const client = new Client({
      apiKey: 'YOUR_API_KEY_HERE',
      environment: 'TEST',
 });

 // intialise the API object with the client object
 const paymentsAPI = new CheckoutAPI(client).PaymentsApi;

Enter fullscreen mode Exit fullscreen mode

The CheckoutAPI object exposes a few different API helpers, we want to use the PaymentsApi. Now we have initialized checkout, we can use it to call the sessions API.

Handle /sessions on the server

First we create an asynchronous function to submit the sessions request. We build our request object with all of the fields we want to pass to our API. In the example below we include the fields we recommend to increase the chances of a frictionless flow.

// ../backend/../payments.ts

async postForSessions(data): Promise<CreateCheckoutSessionResponse> {
 const sessionsRequestData: CreateCheckoutSessionRequest = {
      amount: {
        currency: "EUR",
        value: 1000, 
      },
      countryCode: "NL",
      shopperName: {
        firstName: "FirstName",
        lastName: "LastName",
      },
      telephoneNumber: "0612345678",
      billingAddress: {
        houseNumberOrName: "1",
        street: "Shopper Billing Street",
        city: "Amsterdam",
        country: "NL",
        postalCode: "1234AB",
      },
      deliveryAddress: {
        houseNumberOrName: "1",
        street: "Shopper Delivery Street",
        city: "Amsterdam",
        country: "NL",
        postalCode: "1234AB",
      },
      shopperIP: "shopperIP",
      shopperEmail: "shopperEmail",
      channel: PaymentRequest.ChannelEnum.Web,
      reference: "YOUR_PAYMENT_REFERENCE",
      returnUrl: "https://your-company.com/checkout?shopperOrder=12xy..",
      merchantAccount: "YOUR_MERCHANT_ACCOUNT",
 };

 const sessionsResponse = await this.paymentsAPI.sessions(sessionsRequestData);
 return sessionsResponse;
}

Enter fullscreen mode Exit fullscreen mode

Above we pass the request data to the sessions function on the paymentsAPI object and return the awaited response.

We have now set up our backend for the sessions flow. Let’s review what we did:
✅ We installed the Adyen API library
✅ We configured the Client object with our API key and environment
✅ We instantiated the CheckoutAPI with our Client object and called the PaymentsAPI
✅ We created a function to handle the /sessions API call

Now let’s set up our frontend to send the request.

Install Adyen Web SDK

In our frontend app we will use the Adyen Web SDK so let’s install it by running the command below. These examples are using version 5.55.1.

npm install @adyen/adyen-web --save
Enter fullscreen mode Exit fullscreen mode

To make sure our Drop-in looks as expected we need to import the Adyen web stylesheet in our app. This might look different depending on your set up. Here we will import it in our index.js file. You can override the styling rules to add your own styles. We also import the AdyenCheckout module so we can use it later to create our checkout instance.

// ../frontend../dropin.js

import AdyenCheckout from "@adyen/adyen-web";
import "@adyen/adyen-web/dist/adyen.css";
Enter fullscreen mode Exit fullscreen mode

Configuring the Web Drop-in

Since we are using session flow our Drop-in requires the response from the sessions API call in its configuration. So let’s first make a call to our backend to receive the response from the /sessions endpoint.

//  ../frontend../dropin.js 

const sessionsRequest = {
// data for your request object, may include shopper details, the amount etc...  
}

const sessionsResponse = await postDoSessions(sessionsRequest);

// ../frontend../payments.js

export const postDoSessions = async (data) => {
// send the sessions request to your backend  
const response = await fetch("/api/sessions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ data }),
  });
  return await response.json();

};

Enter fullscreen mode Exit fullscreen mode

Now we have the sessions response we can now build our configuration object and create our checkout instance.

// ../frontend../dropin.js 

// create configuration object to pass into AdyenCheckout
const checkoutConfig = {
    locale: "en_US",
    environment: "test",
    clientKey: YOUR_ADYEN_CLIENT_KEY,
    session: sessionsResponse, // response from the /sessions call 
    onPaymentCompleted: onPaymentCompleted,
    onError: onError,
};

Enter fullscreen mode Exit fullscreen mode

Our configuration object has two events which handle the payment:
onPaymentCompleted is triggered once the payment completes, here you can handle what should happen afterwards.
onError is triggered if an error is thrown. Use this event to gracefully handle errors for your shoppers.

We create callback functions for each which will be executed when the events are invoked by the Drop-in.

// ../frontend../dropin.js

  const onPaymentCompleted = (result, dropin) => {
    // handle payment completed here
  };

  const onError = (error, dropin) => {
    // handle error here
  };

Enter fullscreen mode Exit fullscreen mode

For a full list of all configuration options you can refer to our documentation.

Now we have our configuration object set up with our session data, let's instantiate the Drop-in, pass the configuration to AdyenCheckout and mount the instance as follows:

// ../frontend../dropin.js

const checkout = await AdyenCheckout(checkoutConfig);

// This will mount the checkout component to `<div id="dropin-container"></div>`
checkout.create("dropin").mount("#dropin-container");

Enter fullscreen mode Exit fullscreen mode

The component will be mounted to an element with an id of #dropin-container. For our last step let’s add a div element to our HTML with this id.

// ../frontend../dropin.html

<div id="dropin-container"></div>
Enter fullscreen mode Exit fullscreen mode

Now we’ve set up our frontend to handle payments with Sessions flow and Drop-in. Let’s review what we just did:
✅ We installed the Adyen Web SDK
✅ We called our sessions API session data.
✅ We created our configuration object passing the sessions response object and creating callback functions to handle the events.
✅ We instantiated our checkout object with our config.
✅ We created and mounted our Drop-in to our DOM.

Diagram explaining sessions flow across client, server and Adyen domain

Integrate with Advanced Flow

This guide assumes you already have your Adyen API key and client key and are pointing to our TEST environment. If you haven’t you can find directions here.

Let’s start with configuring the advanced flow on our backend. The advanced flow consists of three API calls /paymentMethods, /payments and /payments/details.

Install Adyen API library

The first step is to install the Adyen API library, these examples will be in TypeScript so we’re going to use the Node.js library. These examples are built using version 15.1.0 of the api-library which utilizes the latest version of Checkout API (71).

If you have npm installed, you install the library in your app by running:

npm install --save @adyen/api-library
npm update @adyen/api-library

Enter fullscreen mode Exit fullscreen mode

Configure CheckoutAPI

Now we have the library installed, let’s write our payment service. The first step is to initialize the Client object with your API key and environment, then we need to pass this client into the CheckoutAPI constructor to initialize our CheckoutAPI object.

// ../backend/../payments.ts

import { Client, CheckoutAPI } from '@adyen/api-library';

// initialise the client object
 const client = new Client({
   apiKey: 'YOUR_API_KEY_HERE',
   environment: 'TEST',
});

// intialise the API object with the client object
const paymentsAPI = new CheckoutAPI(client).PaymentsApi;

Enter fullscreen mode Exit fullscreen mode

The CheckoutAPI object exposes a few different API helpers, we want to use the PaymentsApi.

The advanced flow consists of three API calls, let’s create functions to handle each of these requests.

Handle /paymentMethods on the server

The initial /paymentMethods call is used to get a list of the available payment methods. We need to pass the response of this call to our Drop-in configuration. Which payment methods are returned depends on your merchant configuration. We build our request object with the fields we want to pass to the API. For this example we will just use the required field of merchantAccount. You can find the optional parameters here.

// ../backend/../payments.ts 

async postForPaymentMethods(): Promise<PaymentMethodsResponse> {
    const postData = {
      merchantAccount: 'YOUR_MERCHANT_ACCOUNT',
    };

    const paymentMethodsResponse: PaymentMethodsResponse = await     this.paymentsAPI.paymentMethods(postData);

    return paymentMethodsResponse;
  }

Enter fullscreen mode Exit fullscreen mode

Handle /payments on the server

The /payments request sends the data related to the payment and the shopper. We want to call this when our shopper submits on the Drop-in. Our payments request may look different depending on whether we want to implement the native flow or the redirect flow.

Payment request for Native flow:

If we want to implement the native flow then we need to set the threeDSRequestData object in the AuthenticationData object of the API request like so:

// ../backend/../payments.ts

const authenticationData: AuthenticationData = {
      threeDSRequestData: {
        nativeThreeDS: ThreeDSRequestData.NativeThreeDSEnum.Preferred, 
      },
};

Enter fullscreen mode Exit fullscreen mode

Here we have set nativeThreeDS to be preferred, this will ensure we get the native flow. You can find the other optional parameters here.

Now let’s build the rest of our request object and make the payments request. In the example below we include the fields we recommend to increase the chances of a frictionless flow.

// ../backend/../payments.ts

async postForPaymentsNative(data): Promise<PaymentResponse> {

    const paymentRequestData: PaymentRequest = {
      amount: {
        currency: "EUR",
        value: 1000,
      },
      authenticationData: {
        ...authenticationData, // pass our authenticationData object 
      },
      countryCode: "NL",
      shopperName: {
        firstName: "FirstName",
        lastName: "LastName",
      },
   telephoneNumber: "0612345678",
   billingAddress: {
     houseNumberOrName: "1",
     street: "Shopper Billing Street",
     city: "Amsterdam",
     country: "NL",
     postalCode: "1234AB",
   },
   deliveryAddress: {
      houseNumberOrName: "1",
      street: "Shopper Delivery Street",
      city: "Amsterdam",
      country: "NL",
      postalCode: "1234AB",
   },
      shopperIP: "ShopperIP",
      shopperEmail: "ShopperEmail",
      channel: PaymentRequest.ChannelEnum.Web,
      browserInfo: data.browserInfo, 
      origin: url, 
      paymentMethod: data.paymentMethod, 
   reference: "YOUR_PAYMENT_REFERENCE",
   returnUrl: "https://your-company.com/checkout?shopperOrder=12xy..",
   merchantAccount: "YOUR_MERCHANT_ACCOUNT",
    };

 const paymentResponse: PaymentResponse = await this.paymentsAPI.payments(paymentRequestData);

    return paymentResponse;
  }

Enter fullscreen mode Exit fullscreen mode

Above we pass the request data to the payments function on the paymentsAPI object and return the awaited response.

Payment request for Redirect

Since redirect is the default in advanced flow we don’t need to pass any additional data to our authenticationData object in our payments request.

We can just build our request object and make the payment request. In the example below we include the fields we recommend to increase the chances of a frictionless flow.

// ../backend/../payments.ts

async postForPaymentsRedirect(data): Promise<PaymentResponse> {

    const paymentRequestData: PaymentRequest = {
      amount: {
        currency: "EUR",
        value: 1000,
      },
     countryCode: "NL",
      shopperName: {
        firstName: "FirstName",
        lastName: "LastName",
      },
   telephoneNumber: "0612345678",
   billingAddress: {
     houseNumberOrName: "1",
     street: "Shopper Billing Street",
     city: "Amsterdam",
     country: "NL",
     postalCode: "1234AB",
   },
   deliveryAddress: {
      houseNumberOrName: "1",
      street: "Shopper Delivery Street",
      city: "Amsterdam",
      country: "NL",
      postalCode: "1234AB",
   },
      shopperIP: "ShopperIP",
      shopperEmail: "ShopperEmail",
      channel: PaymentRequest.ChannelEnum.Web,
      browserInfo: data.browserInfo, 
      origin: url, 
      paymentMethod: data.paymentMethod, 
   reference: "YOUR_PAYMENT_REFERENCE",
   returnUrl: "https://your-company.com/checkout?shopperOrder=12xy..",
   merchantAccount: "YOUR_MERCHANT_ACCOUNT",
    };

    const paymentResponse: PaymentResponse = await this.paymentsAPI.payments(paymentRequestData);

    return paymentResponse;
  }


Enter fullscreen mode Exit fullscreen mode

Handling PaymentMethod and BrowserInfo

In both native and redirect examples you may notice we set paymentMethod and browserInfo with the data object passed from the client. This data comes from the Drop-in state and is returned in the onSubmit event callback that we will define later.

Handle /payments/details on the server

The /payments/details request returns the details of the payment request we made. This response will include the result of the Authentication. What we pass to the details call is different depending on native or redirect flow, we will delve into this in the upcoming client section. For now we can create our request like so:

// ../backend/../payments.ts

 async postForPaymentDetails({ details }: { details: PaymentCompletionDetails }): Promise<PaymentDetailsResponse> {
    const paymentDetailsResponse: PaymentDetailsResponse = await this.paymentsAPI.paymentsDetails({ details });

    return paymentDetailsResponse;
  }


Enter fullscreen mode Exit fullscreen mode

We have now set up our backend for the advanced flow. Let’s review what we did:
✅ We installed the Adyen API library
✅ We configured the Client object with our API key and environment
✅ We instantiated the CheckoutAPI with our Client object and called the PaymentsAPI
✅ We created a function to handle the /paymentMethods API call
✅ We created a function to handle the /payments API call
✅ We created a function to handle the /payments/details API call

Now let’s set up our frontend to send the request.

Install Adyen Web SDK

In our frontend app we will use the Adyen Web SDK so let’s install it by running the following command:

npm install @adyen/adyen-web --save
Enter fullscreen mode Exit fullscreen mode

To make sure our Drop-in looks as expected we need to import the Adyen web stylesheet in our app. This might look different depending on your set up. Here we will import it in our index.js file. You can override the styling rules to add your own styles. We also import the AdyenCheckout module that we will use later to create our checkout instance.

// ../frontend/../dropin.js

import AdyenCheckout from "@adyen/adyen-web";
import "@adyen/adyen-web/dist/adyen.css";

Enter fullscreen mode Exit fullscreen mode

Configuring the Web Drop-in

Since we are using advanced flow the first thing our client needs is the paymentMethods response from the /paymentMethods API so we can configure the drop-in.

// ../frontend/../dropin.js

 const paymentMethods = await getPaymentMethods();

// create configuration object to pass into AdyenCheckout
const checkoutConfig = {
      paymentMethodsResponse: paymentMethods,
      locale: "en_US",
      environment: "test",
      clientKey: CLIENT_KEY,
      onSubmit: onSubmit,
      onAdditionalDetails: onAdditionalDetails,
 };

Enter fullscreen mode Exit fullscreen mode

Our configuration object has two events which handle the payment. We create callback functions for each which will be executed when the events are invoked by the Drop-in.

The onSubmit function is triggered when the shopper clicks the Pay button, here you can send the payment request and handle the response actions.

// ../frontend/../dropin.js

const onSubmit = async (state, dropinComponent) => {
  if (state.isValid) {
     const paymentResponse = await postDoPayment(state.data);
     // check result code and handle action here if required  
     if (paymentResponse.resultCode === "Authorised") {
        // no threeDS required
     } else if(paymentResponse.action) {
       dropinComponent.handleAction(paymentResponse.action);    
     }
  }
};

Enter fullscreen mode Exit fullscreen mode

In the code example above you can see we first check the state is valid to ensure there are no errors in the input and then pass the state.data to our payment request. This is the object that has the data for paymentMethod and browserInfo which we set in our API request. We await the payment response, the response will have a resultCode and depending on that resultCode it could also have an action object. An action is an additional step required which can be handled by the Drop-in handleAction method. Once we receive the response we check the resultCode and check if there is an action. Since we are building for 3DS2 we know we can expect an action of either redirect or threeDS2 (for native). For more information on actions you can go refer to our documentation.

The onAdditionalDetails function is triggered if a payment method requires additional details. In our context this event will be invoked to handle the native 3DS2 result.

// ../frontend/../dropin.js

 const onAdditionalDetails = async (state, dropinComponent) => {
      const paymentDetailsResponse = await postDoPaymentDetails(state.data);
      // handle result 
    };

Enter fullscreen mode Exit fullscreen mode

When integrating with the 3DS2 native flow we use the onAdditionalDetails event callback to submit the /payments/details API call and get the final result. The onAddtionalDetails event is automatically triggered in native flow by the Drop-in once the shopper has completed the 3DS2 action.

For a full list of all configuration options you can refer to our documentation. Now we have built our configuration object and handlers for the callbacks, let’s instantiate the drop-in, pass the configuration to AdyenCheckout and mount the instance as follows:

// ../frontend../dropin.js

const checkout = await AdyenCheckout(checkoutConfig);

// This will mount the checkout component to `<div id="dropin-container"></div>`
checkout.create("dropin").mount("#dropin-container");

Enter fullscreen mode Exit fullscreen mode

The component will be mounted to an element with an id of #dropin-container. For our last step let’s add a div element to our HTML with this id.

// frontend/../dropin.html

<div id="dropin-container"></div>
Enter fullscreen mode Exit fullscreen mode

Handling the 3DS2 result

We mentioned above that the onAdditionalDetails callback is automatically triggered for native 3DS2. Here you will get the details result in the state.data object which is the first parameter of the callback event. Pass this data to the /payments/details API to complete the 3DS2 transaction.

If the integration is for 3DS2 redirect then we need to handle the redirect result differently. In a redirect flow when the shopper is redirected back to your merchant page the returnUrl will have the redirectResult appended, it might look something like this:

https://your-company.com/checkout?shopperOrder=12xy&redirectResult=X6XtfGC3%21Y...
Enter fullscreen mode Exit fullscreen mode

In this case we need to parse the url to get the redirectResult and then pass this to our /payments/details call.

// ../frontend/../dropin.js

// example of how to parse redirect result from the url 
const parseRedirectResultToRequestData = (url) => {
    const redirectResult = url.substring(url.indexOf("=") + 1, url.length);
  return {
    details: { redirectResult },
  };
};

const requestData = parseRedirectResultToRequestData(url);
const paymentDetailsResponse = await postDoPaymentDetails(requestData);

Enter fullscreen mode Exit fullscreen mode

And that’s it, now we’ve set up our frontend to handle payments with advanced flow and Drop-in. Let’s review what we just did:
✅ We installed the Adyen Web SDK
✅ We called our paymentMethods API to get the paymentMethods data
✅ We created our configuration object passing the paymentMethods data and creating callback functions to handle the events.
✅ We instantiated our checkout object with our configuration.
✅ We created and mounted our Adyen Drop-in in our DOM.
✅ We handle the 3DS2 result in our onAdditionalDetails event callback in the native flow
✅ We handle the 3DS2 result by parsing the redirectResult from the url in the redirect flow

Diagram explaining advanced flow across client, server and Adyen domain

Mobile integrations

This guide was intended for a browser flow of 3DS2 with Adyen Web. If you wish to integrate with 3DS2 for mobile we highly recommend using one of our dedicated mobile SDKs available for Android or iOS. If you still prefer to use a web integration then we strongly recommend using the redirect flow over native flow.

Conclusion

Thanks for reading! In this blog we’ve covered:
✅ What is 3D Secure 2
✅ How Adyen supports 3DS2 transactions
✅ A comparison between the different integration options
✅ How to integrate using the Sessions flow
✅ How to integrate using the Advanced flow
✅ How to handle 3DS2 on the client-side using Adyen Drop-in

We hope you find this technical blog helpful. Don’t forget to check out the github repository which contains a fully working integration-example. It’s important to note that, while this guide promotes an ideal flow with our recommended best practices, all payment flows are uniquely complicated, so if this guide doesn’t exactly suit your needs, you can refer to our extensive documentation guides.

💖 💪 🙅 🚩
sarahcodes_dev
Sarah 🦄

Posted on January 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related