Skip to main content

Authenticate Merchant Initiated Transactions (MITs) with 3DS

3DS (3D Secure) is an online payment authentication protocol that enhances anti-fraud efforts. In the context of Merchant Initiated Transactions (MIT), such as recurring payments and subscriptions, 3DS operates differently. Instead of requiring cardholders to undergo an additional layer of verification like a one-time password or biometric scan during each transaction, any necessary authentication challenges occur in a decoupled manner, typically during the initial setup of the payment agreement. This approach allows merchants to process subsequent payments seamlessly without direct cardholder interaction, while still complying with Secure Customer Authentication (SCA) requirements under regulations like PSD2.

This guide will show you how to use the Basis Theory Platform to perform 3DS authentication for Merchant Initiated Transactions.

3D Secure is an Enterprise feature. Contact support@basistheory.com to request access.

Getting Started

To get started, you will need to create a Basis Theory Account and a TEST Tenant.

Make sure to use your work email (e.g., john.doe@yourcompany.com)

Provisioning Resources

In this section, we will explore the bare minimum resources necessary to authenticate merchant transactions with 3DS.

Public Application

You will need a Public Application with permissions to create tokens and 3DS sessions. Click here to create one.

This will create an application with the following Permissions:

  • Permissions: token:create, 3ds:session:create
Save the API Key from the created Public Application as it will be used later in this guide.

Private Application

Next, you will need a Private Application for your backend with the permission to authenticate 3DS sessions.

Click here to create it with the following Permissions:

  • Permissions: 3ds:session:authenticate
Save the API Key from the created Private Application as it will be used later in this guide.

Creating a Card Token

In order to run 3DS authentication on a customer card, it must be first tokenized with Basis Theory. Follow the Collect Cards Guide to learn how to create a card token using a variety of different technologies available through the Basis Theory SDKs.

Creating a 3DS Session

First, let's create a 3DS session, pass the created card token id as the token_id property and merchant as the session type.

In this example, we are using Basis Theory SDK and Express framework for Node.js.

const { BasisTheory } = require("@basis-theory/basis-theory-js");
const express = require("express");

const app = express();
const PORT = 3000;

app.use(express.json());

let bt;
(async () => {
bt = await new BasisTheory().init("<PUBLIC_API_KEY>");
})();

app.post("/sessions/create", async (req, res) => {
const { token_id } = req.params;

try {
const session = await bt.threeds.createSession({
tokenId: token_id,
type: "merchant",
});
res.status(201).send(session);
} catch (error) {
console.error('Error while creating 3DS session:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});

// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

Be sure to replace <PUBLIC_API_KEY> with the Public API Key you created in the Public Application step, and pass tokenId to the call as the token id created in the Creating a Card Token step.

Authenticating a 3DS Session

Once the session is created, it must be authenticated. In this process, the merchant must send information about the transaction to the 3DS server.

This is done by calling Authenticate 3DS Session endpoint from your own backend, with the private API key created earlier.

In this example, we are using Basis Theory SDK and Express framework for Node.js.

const { BasisTheory } = require("@basis-theory/basis-theory-js");
const express = require("express");

const app = express();
const PORT = 3000;

app.use(express.json());

let bt;
(async () => {
bt = await new BasisTheory().init("<PUBLIC_API_KEY>");
})();

app.post("/sessions/create", async (req, res) => {
const { token_id } = req.params;

try {
const session = await bt.threeds.createSession({
tokenId: token_id,
type: "merchant",
});
res.status(201).send(session);
} catch (error) {
console.error('Error while creating 3DS session:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});

app.post("/:sessionId/authenticate", async (req, res) => {
const { sessionId } = req.params;

try {
const authentication = await bt.threeds.authenticateSession(sessionId, {
authenticationCategory: "payment",
authenticationType: "other-payment",
decoupledChallengeMaxTime: 10,
purchaseInfo: {
amount: "80000",
currency: "826",
exponent: "2",
date: "20240109141010"
},
requestorInfo: {
id: "example-3ds-merchant",
name: "Example 3DS Merchant",
url: "https://www.example.com/example-merchant"
},
merchantInfo: {
mid: "9876543210001",
acquirerBin: "000000999",
name: "Example 3DS Merchant",
categoryCode: "7922",
countryCode: "826"
},
cardholderInfo: {
name: "John Doe",
email: "john@me.com"
}
}, { apiKey: "<PRIVATE_API_KEY>" });
res.status(200).send(authentication);
} catch (error) {
console.error('Error during authentication:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});

// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

On a production environment, your endpoint to authorize sessions should be behind your own authorization scheme.
Be sure to replace <PRIVATE_API_KEY> with the Private API Key you created previously.

If the status for the authentication response is successful, that means a frictionless authentication happened and the authentication token is available as the authentication_value property.

Decoupled Challenge

If after authenticating a 3DS session, the authentication response status is set as decoupled-challenge, that means that a decoupled challenge is necessary before getting the final 3DS authentication value.

A 3DS decoupled challenge is an authentication method where the cardholder verifies their identity (solves a challenge) separately from the transaction flow—often on a different device or at a different time.

You can specify a time limit for the decoupled challenge to be completed by setting the decoupled_challenge_max_time property in the authentication request.

Verifying Decoupled Challenge Completion

Since the decoupled challenge is handled by the card issuer, you must create a Webhook to receive a notification when the challenge is completed.

Check our official documentation on all the different manners you can create a Webhook with Basis Theory.

For the 3DS decoupled challenge, you should create a Webhook that looks for the 3ds.session.decoupled-challenge-notification event type.

Retrieving a Challenge Result

Once a challenge is complete and a decoupled challenge notification is received, results are retrieved by calling the Get Challenge Result endpoint from your backend.

This is done by calling the Basis Theory backend endpoint /3ds/{sessionId}/sessions/challenge-result from your own backend, using the same private API key that was used to authenticate.

In this example, we are using Basis Theory SDK and Express framework for Node.js.

const { BasisTheory } = require("@basis-theory/basis-theory-js");
const express = require("express");

const app = express();
const PORT = 3000;

app.use(express.json());

let bt;
(async () => {
bt = await new BasisTheory().init("<PUBLIC_API_KEY>");
})();

app.post("/sessions/create", async (req, res) => {
const { token_id } = req.params;

try {
const session = await bt.threeds.createSession({
tokenId: token_id,
type: "merchant",
});
res.status(201).send(session);
} catch (error) {
console.error('Error while creating 3DS session:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});

app.post("/:sessionId/authenticate", async (req, res) => {
const { sessionId } = req.params;

try {
const authentication = await bt.threeds.authenticateSession(sessionId, {
authenticationCategory: "payment",
authenticationType: "other-payment",
decoupledChallengeMaxTime: 10,
purchaseInfo: {
amount: "80000",
currency: "826",
exponent: "2",
date: "20240109141010"
},
requestorInfo: {
id: "example-3ds-merchant",
name: "Example 3DS Merchant",
url: "https://www.example.com/example-merchant"
},
merchantInfo: {
mid: "9876543210001",
acquirerBin: "000000999",
name: "Example 3DS Merchant",
categoryCode: "7922",
countryCode: "826"
},
cardholderInfo: {
name: "John Doe",
email: "john@me.com"
}
}, { apiKey: "<PRIVATE_API_KEY>" });
res.status(200).send(authentication);
} catch (error) {
console.error('Error during authentication:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});

app.get("/:sessionId/challenge-result", async (req, res) => {
const { sessionId } = req.params;

try {
const result = await bt.threeds.getChallengeResult(sessionId, { apiKey: "<PRIVATE_API_KEY>" });
res.status(200).send(result);
} catch (error) {
console.error('Error getting challenge result:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});

// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

That's it 🎉! The result from the authentication (in case of frictionless) or challenge-result calls contains the authentication token (authentication_value attribute) and any other information needed to fully process the 3DS transaction.

Learn More