Are you looking for test card numbers?

Would you like to contact support?

Default icon

Drop-in tutorial: Node.js + Express

Build a complete integration for accepting payments on your website.

  Check out the full project in our Drop-in and Node.js example repository on GitHub.

This tutorial explains the server- and client-side steps for integrating Drop-in, our complete UI solution for accepting payments on your website. Integrating Drop-in is the quickest way to offer multiple payment methods, without having to build each payment method individually.

After completing this tutorial, you'll have a base integration that supports all payment flows, and can be quickly extended to support most payment methods.

To learn more about our available integration options, refer to Online payments.

How it works

When your shopper visits your checkout page, your server makes an API call to Adyen to get a list of available payment methods for the shopper. The shopper then selects a payment method from that list, and enters their payment details in the input fields rendered by Drop-in.

When the shopper selects the Pay button, your server uses the data returned by the client to submit a payment request to Adyen. If the response requires additional action from the shopper, Drop-in can handle these additional actions. In such cases, your server will make another request to Adyen to complete or verify the payment. When no additional actions are required, the payment result can be presented to the shopper.

Overview of the build process

Prefer to watch a video tutorial?

Check out our Drop-in tutorial: Node.js + Express on YouTube.

After following this tutorial, you'll have a complete checkout solution including both server- and client-side implementations.

Server implementation

You'll begin by exposing a few endpoints on your server to handle all shopper interactions at checkout. The names of your server endpoints in this tutorial match those in Adyen's example integrations. You can name these endpoints however you like.

HTTP method Your server endpoint Role Related Adyen API endpoint
GET /api/getPaymentMethods Gets the available payment methods. /paymentMethods
POST /api/initiatePayment Sends payment parameters together with the input details collected from the shopper. /payments
POST, GET /api/handleShopperRedirect Sends payment details during a page redirect and determines the outcome for the shopper.

Used for all payment methods that require a redirect, for example iDEAL or Klarna.
POST /api/submitAdditionalDetails Sends payment details when a shopper is required to perform additional actions without leaving your website.

Examples: Native 3D Secure 2 authentication, native QR (WeChat Pay), and overlays (PayPal)

These endpoints are custom routes (URI patterns) that interact with the Adyen API at specific points in the checkout process, and are accessed by your client through fetch() requests. With the exception of /handleShopperRedirect, each endpoint returns a JSON response data back to the client.

Client implementation

After implementing the back end, you'll implement the front end with a configurable, ready-to-use Drop-in UI component.

You'll first add the necessary library and styles to your checkout page. Then, you'll create fetch() requests and event handlers to facilitate the app's client-server interactions. Finally, you'll configure and instantiate Drop-in mounted to your checkout page. You'll then be ready to start testing transactions with hundreds of payment methods from around the world.

Before you begin

This tutorial shows both server- and client-side integration steps for Drop-in. To be well prepared, you should have experience working with Node.js, Express, and a template system (such as Handlebars).

Before you begin your integration, make sure you have the following tools and credentials:

Item Description
Adyen test account This gives you access to the test Customer Area.
API key Private API key used to authenticate API calls to the Adyen payments platform.
Client key Public key used to authenticate requests from your web environment.
Adyen server-side library for Node.js API library that allows you to easily work with Adyen's APIs.

Make sure you have the latest versions of the Checkout API and the Node.js library.

Build the server

You'll first implement the back end to make sure your server is ready for the checkout process.

Install dependencies

The integration on this page uses Node.js and Express, with Handlebars templating. For a smoother integration experience, the examples on this page use the following libraries as well:

npm package Description
express-handlebars Handlebars template (view) engine for Express.
uuid Creates unique identifiers in RFC4122 UUID format.

In the root of your project, install the dependencies:

npm install --save express @adyen/api-library express-handlebars uuid

Configure the server

After installing the dependencies, import the modules and configure the Adyen client in your server file (we use index.js in this example). Provide your API key as a string in the boilerplate code or fetch it from the environment. You can use a library like dotenv to load environment variables when the application loads.

const express = require("express");
const path = require("path");
const hbs = require("express-handlebars");
const { v4: uuidv4 } = require('uuid');
const { Client, Config, CheckoutAPI } = require("@adyen/api-library");
const app = express();

// Parse JSON bodies
// Parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));
// Serve client from build folder
app.use(express.static(path.join(__dirname, "/public")));

// Adyen Node.js API library boilerplate (configuration, etc.)
const config = new Config();
config.apiKey = process.env.ADYEN_API_KEY;
const client = new Client({ config });
const checkout = new CheckoutAPI(client);

// Use Handlebars as the view engine
app.set("view engine", "handlebars");

// Specify where to find view files
    defaultLayout: "main",
    layoutsDir: __dirname + "/views/layouts"

/* ################# API ENDPOINTS ###################### */

/* ################# CLIENT SIDE ENDPOINTS ###################### */

// Start server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));

Next, you'll expose a few API endpoints on your server to support different points in the checkout process.

Get available payment methods

The first point in the checkout process: the shopper is ready to pay and looks to select a payment method.

To get the available payment methods for the shopper, you'll expose an endpoint on your server by creating a GET route to /api/getPaymentMethods. When this endpoint is accessed, the instance of checkout will make a call to Adyen's /paymentMethods endpoint.

Pass in your merchantAccount and channel:Web. You can also specify amount and countryCode parameters— we'll use these to tailor the list of available payment methods to your shopper.

Next, you'll pass the entire JSON response from your server back to your client.

getPaymentMethods"/api/getPaymentMethods", async (req, res) => {
  try {
    const response = await checkout.paymentMethods({
      channel: "Web",
      merchantAccount: process.env.MERCHANT_ACCOUNT,
  } catch (err) {
    console.error(`Error: ${err.message}, error code: ${err.errorCode}`);

An array of available payment methods is returned in the response, each with a name and type. Drop-in uses the response to show the available payment methods to the shopper.

/paymentMethods response
      "name":"Credit Card",
      // ...
    // ...

We'll revisit this response object later on the client side when we configure Drop-in.

Initiate a payment

At this point in the checkout process, the shopper selects a payment method, enters their payment details, and selects the Pay button.

To initiate the payment, you'll expose an endpoint on your server by creating a POST route to /api/initiatePayment. When this endpoint is accessed, a call will be made to Adyen's /payments endpoint.

In our example, the shopper selects to pay with their card. Specify the amount, reference, merchantAccount, paymentMethod, channel, returnUrl, and browserInfo in the request body. Note that the values of certain parameters, such as browserInfo and paymentMethod, represent data passed from the client when performing the fetch() request.

A sample request for a credit card payment (with 3D Secure redirect authentication) of 10 EUR:

// A temporary store to keep payment data to be sent in additional payment details and redirects.
// This is more secure than a cookie. In a real application this should be in a database.
const paymentDataStore = {};"/api/initiatePayment", async (req, res) => {
  try {
    // unique ref for the transaction
    const orderRef = uuidv4();
    // Ideally the data passed here should be computed based on business logic
    const response = await checkout.payments({
      amount: { currency: "EUR", value: 1000 }, // value is 10€ in minor units
      reference: orderRef, // required
      merchantAccount: process.env.MERCHANT_ACCOUNT, // required
      channel: "Web", // required
      // we pass the orderRef in return URL to get paymentData during redirects
      returnUrl: `http://localhost:8080/api/handleShopperRedirect?orderRef=${orderRef}`, // required for redirect flow
      browserInfo: req.body.browserInfo,
      paymentMethod: req.body.paymentMethod // required

    const { action } = response;

    if (action) {
      paymentDataStore[orderRef] = action.paymentData;
  } catch (err) {
    console.error(`Error: ${err.message}, error code: ${err.errorCode}`);

If the response contains an action object, this means that additional client-side actions are required to complete the payment. As shown above, you'll pass the response from the server to the client, where it will be handled by Drop-in. You'll also temporarily save paymentData (for example, in a database, or in a cache as shown in this example). You'll need to include the paymentData in your next API request to check the payment result in case of a page redirection.

/payments response with an action object
   // ...

We'll revisit this action object later on the client side when we configure Drop-in to handle the action.

If there's no action object in the response, no additional actions are needed to complete the payment. This means that the transaction has reached a final status (for example, in a 3D Secure scenario, the transaction was either exempted or out-of-scope for 3D Secure authentication). Pass the response to your client — you'll use this to present the payment result to your shopper.

Handle the redirect

At this point in the checkout process, the shopper has been redirected to another site to complete the payment, and back to your returnUrl using either an HTTP POST or GET. This redirection is a result of Drop-in performing the action.type: redirect specified in the /payments response.

After the shopper is redirected back, you need to verify or complete the payment using data from the redirected page. You'll do that by exposing two more endpoints on your server, representing a GET and POST route to /api/handleShopperRedirect.

When either endpoint is accessed, a call will be made to Adyen's /payments/details endpoint. In the request, specify:

  • paymentData: the value from the /payments response.
  • details: data returned from redirected page.
// handle both POST & GET requests
app.all("/api/handleShopperRedirect", async (req, res) => {
  // Create the payload for submitting payment details
  const orderRef = req.query.orderRef;
  const redirect = req.method === "GET" ? req.query : req.body;
  const details = {};
  if (redirect.redirectResult) {
    details.redirectResult = redirect.redirectResult;
  } else {
    details.MD = redirect.MD;
    details.PaRes = redirect.PaRes;

  const payload = {
    paymentData: paymentDataStore[orderRef],

  try {
    const response = await checkout.paymentsDetails(payload);
    // Conditionally handle different result codes for the shopper
    switch (response.resultCode) {
      case "Authorised":
      case "Pending":
      case "Received":
      case "Refused":
  } catch (err) {
    console.error(`Error: ${err.message}, error code: ${err.errorCode}`);

You'll receive a response with a pspReference and a resultCode, the latter of which can be used to present the payment result to the shopper.

Successful response

 "pspReference": "88154795347618C",
 "resultCode": "Authorised"

If you receive another action object in the response, pass this back to the client again.

We'll configure the Drop-in to handle additional actions in case you receive more action objects in the response.

Handle other additional details

For some payment flows, the shopper needs to perform additional steps without leaving your website. For example, this can mean scanning a QR code, or completing a challenge in case of native 3D Secure 2 authentication.

To handle submitting these additional details, you'll expose another endpoint on your server by creating a POST route to /api/submitAdditionalDetails. Similar to /api/handleShopperRedirect above, specify details and paymentData parameters in the call to Adyen's /payments/details endpoint. Note that this time, the shopper stays in your website so there is no view redirect logic in the server. Instead, any additional action objects are passed back to the client. The result code is also passed to the client, and this will be used to present the payment result to the shopper.

submitAdditionalDetails"/api/submitAdditionalDetails", async (req, res) => {
  // Create the payload for submitting payment details
  const payload = {
    details: req.body.details,
    paymentData: req.body.paymentData,

  try {
    // Return the response back to client (for further action handling or presenting result to shopper)
    const response = await checkout.paymentsDetails(payload);
  } catch (err) {
    console.error(`Error: ${err.message}, error code: ${err.errorCode}`);

We'll configure the Drop-in to handle additional actions in case you receive more action objects in the response.

Handle result codes

When working with payment methods involving a page redirect, we handled the redirect logic in the server. In all other cases, however, result codes will be handled in the client, where you can present the payment outcome for your shopper. We'll take a look at the logic for handling result codes next when we build the client.

Build the client

After implementing the server-side code, we'll now implement an interactive UI component on the front end of your application.

Add the Drop-in script and styles

To add Drop-in to your checkout page, install the Adyen Web Node package or use a <script> tag:

  1. Include the following script in the <body> above any other JavaScript in your checkout page: 

    We recommend that you also validate the Subresource Integrity (SRI) hash, which we provide for each version of our JavaScript and CSS files. You can find the SRI hashes in our release notes, under Updating to this version.

    <script src="{VERSION}/adyen.js"
    <!-- Adyen provides the SRI hash that you include as the integrity attribute.-->
    <!-- Refer to our release notes to get the SRI hash for the specific version: -->
  2. Use the following CSS file:

    <link rel="stylesheet" href="{VERSION}/adyen.css"
    <!-- Adyen provides the SRI hash that you include as the integrity attribute. -->
    <!-- Refer to our release notes to get the SRI hash for the specific version: -->

    You can add your own styling by overriding the rules in this CSS file.

Create the container element

Next, create a DOM element on your checkout page with a descriptive id value. Place this element where you want Drop-in to be rendered on the page.

<!-- payment.handlebars -->

<div id="dropin-container"></div>

Later on, we'll mount Drop-in to this container.

Connect to your server

At this point, your server is already built to make the necessary API calls to Adyen corresponding to all shopper interactions at checkout. You'll handle this on the front end by building functions for any client-server interactions.

First, define a function that calls your server endpoints and passes JSON-stringified data from the client. We'll call this function callServer. The function uses a JavaScript fetch() request to communicate with the server. We'll later pass the url parameter based on which endpoint is requested, for example /api/initiatePayment or /api/submitAdditionalDetails.

async function callServer(url, data) {
  const res = await fetch(url, {
    method: "POST",
    body: data ? JSON.stringify(data) : "",
    headers: {
      "Content-Type": "application/json",

  return await res.json();

Next, we need a function that determines how to handle the responses sent from the server to the client. We'll call this function handleServerResponse. The function first checks for an action object in the API response, which Drop-in will handle. If there is no action object in the API response, use the resultCode to present the payment result to your shopper. For a full list of result codes, refer to Result codes.

function handleServerResponse(res, component) {
  if (res.action) {
  } else {
    switch (res.resultCode) {
      case "Authorised":
        window.location.href = "/result/success";
      case "Pending":
      case "Received":
        window.location.href = "/result/pending";
      case "Refused":
        window.location.href = "/result/failed";
        window.location.href = "/result/error";

We'll use these functions to create a flexible event handler on the page.

Create the event handler

During the checkout process, we need to handle certain events such as when the shopper clicks the Pay button, or when additional information is required to complete the payment.

We can wrap the above client-server functions nicely in a single, reusable function. We'll call this function handleSubmission. The function calls the server at the provided endpoint and passes the payment data.

async function handleSubmission(state, component, url) {
  try {
    const res = await callServer(url,;
    handleServerResponse(res, component);
  } catch (error) {

After the request is resolved, Drop-in handles the data returned in the Adyen API response.

Configure Drop-in

After building the client-server interaction, we'll now configure and instantiate Drop-in.

Create a configuration object with the following parameters:

Parameter name Required Description
paymentMethodsResponse -white_check_mark- The response from your /api/getPaymentMethods request.
clientKey -white_check_mark- Public key used to authenticate requests from your web environment.
For more information, refer to client side authentication.
locale -white_check_mark- The shopper's locale. This is used to set the language rendered in the UI. For a list of supported locales, see Localization.
environment -white_check_mark- Use test. When you're ready to accept live payments, change the value to one of our live environments
onSubmit -white_check_mark- Assign the reusable event handler handleSubmission with your /api/initiatePayment endpoint. The onSubmit event is called when the shopper selects the Pay button.
onAdditionalDetails -white_check_mark- Assign the reusable event handler handleSubmission with your /api/submitAdditionalDetails endpoint. The onAdditionalDetails event is called if no final state was reached, or additional details are required.
paymentMethodsConfiguration Required or optional configuration for specific payment methods. For more information about payment method-specific configurations, refer to our payment method guides.

For our cards example, we'll include optional configuration - such as showPayButton, hasHolderName, holderNameRequired, name, and amount. For a full list of configurable fields, refer to optional Card configuration.

Let us create this inside an async function so that we can call other async functions with the await syntax.

Drop-in configuration
async function initCheckout() {
  try {
    const paymentMethodsResponse = await callServer("/api/getPaymentMethods");
    const configuration = {
      paymentMethodsResponse: paymentMethodsResponse,
      clientKey: "YOUR_CLIENT_KEY",
      locale: "en_US",
      environment: "test",
      paymentMethodsConfiguration: {
        card: {
          showPayButton: true,
          hasHolderName: true,
          holderNameRequired: true,
          name: "Credit or debit card",
          amount: {
            value: 1000,
            currency: "EUR"
      onSubmit: (state, component) => {
        if (state.isValid) {
          handleSubmission(state, component, "/api/initiatePayment");
      onAdditionalDetails: (state, component) => {
        handleSubmission(state, component, "/api/submitAdditionalDetails");
  } catch (error) {

Instantiate and mount Drop-in

After all the configuration are set, you are ready to instantiate your Drop-in. Create an instance of AdyenCheckout with your newly-created configuration object. You'll use the returned value to create and mount the instance of your UI Drop-in. Finally invoke the initCheckout function and we are done.

async function initCheckout() {
  try {
    // ...
    const configuration = // ...

    const checkout = new AdyenCheckout(configuration);
  } catch (error) {
    // ...


You can checkout the full Node.js example integration on our GitHub examples repository.

Next steps

If you followed this tutorial, you now have an integration built to support all payment flows.

Adding more payment methods to Drop-in does not require building an entirely new integration. Your existing Drop-in code can be quickly extended to support your shoppers' preferred payment methods.