3DS Implementation & Testing (CIT with Redirection)
Customer Initiated Transactions (CIT) are 3DS transactions in which the customer is present during the transaction to complete any required external authorizations, such as a challenge.
This guide covers how to implement 3DS CIT with redirection, meaning the customer is redirected to the Basis Theory 3DS authentication page to complete the authentication process. If you are looking for a guide on how to implement 3DS CIT without redirection, embedding the authentication flow in your checkout page, please refer to the regular implementation guide here.
If you perform transactions without the customer present at checkout, these are classified as Merchant-Initiated Transactions (MIT). For guidance on implementing 3DS MIT, please refer to the implementation guide here.
Summary
This guide provides a comprehensive overview of determining when 3D Secure (3DS) authentication is required, properly implementing the authentication flow, handling customer redirection, and testing your implementation.
When is 3DS Required?
It is important to keep in mind every business has its own bespoke needs when it comes to risk, fraud, and compliance, and all of these variables impact its decision on how to best implement the 3DS authentication pattern for your business.
Triggering 3DS Considerations
Basis Theory simplifies this determination through the authentication field provided in the card token details object.
When this value is sca_required, this suggests the underlying Issuer will require 3DS to be present during the transaction.
While this could likely be true, there are a few considerations and scenarios to consider as you decide how to best optimize for triggering 3DS and optimizing Payment Authorization success:
- When a region always requires 3DS:
- Consider always triggering 3DS when
card.authenticationissca_requiredon your Token or Token Intent's card details object.
- Consider always triggering 3DS when
- When a region does not always require 3DS:
- Consider triggering the 3DS flow after a declined payment authorization to retry the transaction quickly while the customer is still engaged.
- When receiving inconsistent payment authorization declines for Authentication data:
- Consider triggering a frictionless only 3DS flow for these regions or BINs before Authorization. If a challenge is required, consider moving forward with your Authorization without the 3DS Authentication.
- Consider triggering the 3DS flow after a declined authorization to retry the transaction quickly while the customer is still engaged.
Regulatory Context
Note: Always verify regulatory requirements specific to your region and confirm with your PSP to ensure compliance and avoid declined transactions.
Certain regions typically mandate 3DS authentication, notably:
- European Economic Area (EEA)
- United Kingdom (UK)
- Other regions adopting Strong Customer Authentication (SCA): Australia, India, Brazil, and parts of Asia
Provisioning Resources
To successfully process 3DS authentications, you must first create two applications in the Basis Theory Portal: a Public Application and a Private Application. These applications will handle creating, authenticating, and retrieving 3DS sessions.
Public Application
The Public Application provides the public API key needed to create a 3DS session and construct the redirect URL that customers will be redirected to for 3DS authentication.
- Permissions Required:
3ds:session:create
Click here to create the Public Application
Important: Save the generated API Key. You'll use it later in this guide.
Private Application
The Private Application is used by your backend system to retrieve 3DS sessions and challenge results.
- Permissions Required:
3ds:session:read,3ds:session:authenticate
Click here to create the Private Application
Important: Save the generated API Key. You'll use it later in this guide.
Creating a CIT 3DS Session
Unlike the regular CIT Implementation, the 3DS session with redirection is created from your backend instead of the client-side.
It's important to note that the callback_urls parameter and authentication_request are required when creating the session if you are using redirection.
Here's the recommended minimum payload for creating a 3DS session with redirection in most scenarios:
{
token_id: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type: "customer",
device: "browser", // always use browser for 3DS with redirection
authentication_request: {
"authentication_category": "payment",
"authentication_type": "payment-transaction",
"challenge_preference": "no-challenge", // or challenge-requested if challenge is preferred (not guaranteed).
"card_brand": "Visa", // optional for co-badged cards
"merchant_info": {
"mid": "9876543210001",
"acquirer_bin": "000000999",
"name": "Example 3DS Merchant",
"country_code": "826",
"category_code": "5999",
"url": "https://example.com",
},
"requestor_info": { // if accepting American Express, Discover or Cartes Bancaires
"amex_requestor_type": "MER", // if American Express is accepted
"cb_siret_number": "78467169500087", // If Cartes Bancaires is accepted
},
"purchase_info": {
"amount": "80000",
"currency": "826",
"exponent": "2",
"date": "20250101141010",
"transaction_type": "purchase",
"installment_count": "5", // only for installment transactions
"recurring_expiration": "20250131", // only for recurring transactions
"recurring_frequency": "30", // only for recurring transactions
},
"cardholder_info": {
"name": "John Doe",
"email": "john@example.com",
}
},
callback_urls: {
success: "http://localhost:3000/success",
failure: "http://localhost:3000/failure"
}
}
And below is how you can use this object to create a 3DS session with the Basis Theory API and SDKs:
- cURL
- Node
- .NET
- Python
- Go
curl "https://api.test.basistheory.com/3ds/sessions" \
-H "BT-API-KEY: <PRIVATE_API_KEY>" \
-H "Content-Type: application/json" \
-X "POST" \
-d '{
"token_id": "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
"type": "customer",
"device": "browser",
"authentication_request": {
"authentication_category": "payment",
"authentication_type": "payment-transaction",
"challenge_preference": "no-challenge",
"card_brand": "Visa",
"merchant_info": {
"mid": "9876543210001",
"acquirer_bin": "000000999",
"name": "Example 3DS Merchant",
"category_code": "5999",
"country_code": "826",
"url": "https://example.com"
},
"requestor_info": {
"amex_requestor_type": "MER",
"cb_siret_number": "78467169500087"
},
"purchase_info": {
"amount": "80000",
"currency": "826",
"exponent": "2",
"date": "20240109141010",
"transaction_type": "purchase"
},
"cardholder_info": {
"name": "John Doe",
"email": "john@example.com"
}
},
"callback_urls": {
"success": "https://example.com/success",
"failure": "https://example.com/failure"
}
}'
In this example, we are using Basis Theory SDK and Express framework for Node.js.
const express = require("express");
const { BasisTheoryClient } = require("@basis-theory/node-sdk")
const app = express();
const PORT = 3000;
app.use(express.json());
let bt;
(async () => {
// initialize the SDK
bt = await new BasisTheoryClient({ apiKey: "<PRIVATE_API_KEY>", environment: BasisTheoryEnvironment.Test });
// start the server (after SDK is initialized so we don't drop any requests
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
})();
app.post("/create-session", async (req, res) => {
try {
const session = await bt.threeds.sessions.create({
tokenId: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type: "customer",
device: "browser",
authenticationRequest: {
authenticationCategory: "payment",
authenticationType: "payment-transaction",
challengePreference: "no-challenge",
// cardBrand: "Visa", // optional - used if co-badged card
merchantInfo: {
mid: "9876543210001",
acquirerBin: "000000999",
name: "Example 3DS Merchant",
categoryCode: "5999",
countryCode: "826",
url: "https://example.com",
},
requestorInfo: { // if accepting American Express, Discover or Cartes Bancaires
amexRequestorType: "MER", // if American Express is accepted
cbSiretNumber: "78467169500087", // if Cartes Bancaires is accepted
},
purchaseInfo: {
amount: "80000",
currency: "826",
exponent: "2",
date: "20240109141010",
transactionType: "purchase",
//installmentCount: "5", // only for installment transactions
//recurringExpiration: "20250131", // only for recurring transactions
//recurringFrequency: "30", // only for recurring transactions
},
cardholderInfo: {
name: "John Doe",
email: "john@example.com"
}
},
callbackUrls: {
success: "https://example.com/success",
failure: "https://example.com/failure"
}
});
res.status(200).send(session);
} catch (error) {
console.error('Error during session creation:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});
In this example, we are using Basis Theory SDK and ASP.NET Core Framework.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using BasisTheory.Client;
using BasisTheory.Client.Threeds;
namespace server.Controllers
{
public class Program
{
public static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://0.0.0.0:4242")
.UseWebRoot("public")
.UseStartup<Startup>()
.Build()
.Run();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddNewtonsoftJson();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseStaticFiles();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
[ApiController]
public class ThreeDsApiController : Controller
{
private readonly BasisTheory _client;
public ThreeDsApiController()
{
_client = new BasisTheory(apiKey: "<PRIVATE_API_KEY>", clientOptions: new ClientOptions
{
BaseUrl = BasisTheoryEnvironment.Test
});
}
[HttpPost("create-session")]
public async Task<ActionResult> CreateSession()
{
try
{
var session = await _client.Threeds.Sessions.CreateAsync(new CreateThreeDsSessionRequest
{
TokenId = "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
Type = "customer",
Device = "browser",
AuthenticationRequest = new AuthenticateThreeDsSessionRequest
{
AuthenticationCategory = "payment",
AuthenticationType = "payment-transaction",
ChallengePreference = "no-challenge",
// CardBrand = "Visa", // optional - used if co-badged card
MerchantInfo = new ThreeDsMerchantInfo
{
Mid = "9876543210001",
AcquirerBin = "000000999",
Name = "Example 3DS Merchant",
CategoryCode = "5999",
CountryCode = "826",
Url = "https://example.com"
},
RequestorInfo = new ThreeDsRequestorInfo // if accepting American Express, Discover or Cartes Bancaires
{
AmexRequestorType = "MER", // if American Express is accepted
CbSiretNumber = "78467169500087", // if Cartes Bancaires is accepted
},
PurchaseInfo = new ThreeDsPurchaseInfo
{
Amount = "80000",
Currency = "826",
Exponent = "2",
Date = "20240109141010",
TransactionType = "purchase",
// InstallmentCount = "5", // only for installment transactions
// RecurringExpiration = "20250131", // only for recurring transactions
// RecurringFrequency = "30", // only for recurring transactions
},
CardholderInfo = new ThreeDsCardholderInfo
{
Name = "John Doe",
Email = "john@example.com"
}
},
CallbackUrls = new ThreeDsCallbackUrls
{
Success = "https://example.com/success",
Failure = "https://example.com/failure"
}
});
if (session == null)
{
return Problem("Error during session creation.");
}
return Ok(session);
}
catch (Exception ex)
{
Console.WriteLine($"Error during session creation: {ex.Message}");
return Problem("Internal Server Error");
}
}
}
}
In this example, we are using Basis Theory SDK and Flask Framework.
import os
from flask import Flask, request, jsonify
from basis_theory import (
BasisTheory,
ThreeDsMerchantInfo,
ThreeDsRequestorInfo,
ThreeDsPurchaseInfo,
ThreeDsCardholderInfo,
AuthenticateThreeDsSessionRequest,
ThreeDsCallbackUrls
)
app = Flask(__name__)
client = BasisTheory(api_key="<PRIVATE_API_KEY>", environment=BasisTheoryEnvironment.TEST)
@app.route('/create-session', methods=['POST'])
def create_session():
session = client.threeds.sessions.create(
token_id="d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type="customer",
device="browser",
authentication_request=AuthenticateThreeDsSessionRequest(
authentication_category="payment",
authentication_type="payment-transaction",
challenge_preference="no-challenge",
# card_brand="Visa", # optional - used if co-badged card
merchant_info=ThreeDsMerchantInfo(
mid="9876543210001",
acquirer_bin="000000999",
name="Example 3DS Merchant",
category_code="5999",
country_code="826",
url="https://example.com"
),
requestor_info=ThreeDsRequestorInfo( # if accepting American Express, Discover or Cartes Bancaires
amex_requestor_type="MER", # if American Express is accepted
cb_siret_number="78467169500087" # if Cartes Bancaires is accepted
),
purchase_info=ThreeDsPurchaseInfo(
amount="80000",
currency="826",
exponent="2",
date="20240109141010",
transaction_type="purchase",
# installment_count="5", # only for installment transactions
# recurring_expiration="20250131", # only for recurring transactions
# recurring_frequency="30", # only for recurring transactions
),
cardholder_info=ThreeDsCardholderInfo(
name="John Doe",
email="john@example.com"
),
),
callback_urls=ThreeDsCallbackUrls(
success="https://example.com/success",
failure="https://example.com/failure"
)
)
if session is None:
return jsonify({"error": "Error during session creation."}), 500
return jsonify(session.to_dict()), 200
if __name__ == '__main__':
app.run(port=4242, debug=True)
In this example, we are using Basis Theory SDK, Go HTTP package and the Gorilla Mux Router.
package main
import (
"context"
"encoding/json"
"github.com/Basis-Theory/go-sdk"
basistheoryclient "github.com/Basis-Theory/go-sdk/client"
"github.com/Basis-Theory/go-sdk/option"
"github.com/Basis-Theory/go-sdk/threeds"
"github.com/gorilla/mux"
"log"
"net/http"
)
var client *basistheoryclient.Client
func main() {
client := basistheoryclient.NewClient(
option.WithAPIKey("<API_KEY>"),
)
router := mux.NewRouter()
router.HandleFunc("/create-session", createSession).Methods("POST")
addr := "localhost:4242"
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, router))
}
func pointerToString(s string) *string {
return &s
}
func createSession(rw http.ResponseWriter, r *http.Request) {
session := client.Threeds.Sessions.Create(context.Background(), &threeds.CreateThreeDsSessionRequest{
TokenId: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
Type: "customer",
Device: "browser",
CallbackUrls: &threeds.ThreeDsCallbackUrls{
Success: pointerToString("https://example.com/success"),
Failure: pointerToString("https://example.com/failure"),
},
})
authRequest := &threeds.AuthenticateThreeDsSessionRequest{
AuthenticationCategory: pointerToString("payment"),
AuthenticationType: pointerToString("payment-transaction"),
ChallengePreference: pointerToString("no-challenge"),
// CardBrand: pointerToString("Visa"), // optional - used if co-badged card
}
merchantInfo := &basistheory.ThreeDsMerchantInfo{
Mid: pointerToString("9876543210001"),
AcquirerBin: pointerToString("000000999"),
Name: pointerToString("Example 3DS Merchant"),
CategoryCode: pointerToString("5999"),
CountryCode: pointerToString("826"),
Url: pointerToString("https://example.com"),
}
requestorInfo := &basistheory.ThreeDsRequestorInfo{ // if accepting American Express, Discover or Cartes Bancaires
AmexRequestorType: pointerToString("MER"), // if American Express is accepted
CbSiretNumber: pointerToString("78467169500087"), // if Cartes Bancaires is accepted
}
purchaseInfo := &basistheory.ThreeDsPurchaseInfo{
Amount: pointerToString("80000"),
Currency: pointerToString("826"),
Exponent: pointerToString("2"),
Date: pointerToString("20240109141010"),
TransactionType: pointerToString("purchase"),
// InstallmentCount: pointerToString("5"), // only for installment transactions
// RecurringExpiration: pointerToString("20250131"), // only for recurring transactions
// RecurringFrequency: pointerToString("30"), // only for recurring transactions
}
cardholderInfo := &basistheory.ThreeDsCardholderInfo{
Name: pointerToString("John Doe"),
Email: pointerToString("john@example.com"),
}
authRequest.MerchantInfo = merchantInfo
authRequest.RequestorInfo = requestorInfo
authRequest.PurchaseInfo = purchaseInfo
authRequest.CardholderInfo = cardholderInfo
session.AuthenticationRequest = authRequest
createResponse, createErr := client.Threeds.Sessions.Create(context.Background(), session)
if createErr != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": createErr.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(createResponse)
}
3DS sessions that are not authenticated will expire according to the token or token-intent's designated expiration date.
If no expiration date is set, the session expires after one hour. Once a session is successfully authenticated, it remains active indefinitely.
Populating the Authentication Request
The authentication_request parameter is a JSON object that contains the 3DS authentication request details.
It is the accepted practice that the more information you provide to a 3DS session, the higher the likelihood of a frictionless authentication.
Continue reading to learn more about each property in the Authentication Request object and how to best utilize them for your specific use case.
Authentication Category
For the authentication_category field, there are two authentication categories available - In most 3DS scenarios, you'll select the payment category, indicating that a current or future financial transaction (including $0 authentications) is intended.
| Authentication Category | Description |
|---|---|
payment | Utilized when a future financial transaction is intended (including $0 authentications). |
non-payment | Utilized when the authentication is unrelated to an actual or anticipated charge. If you believe you need to utilize this category, please reach out to confirm your use-case. |
Authentication Type
For the authentication_type field, there are three most common types available. The below table outlines the common scenarios for most merchants and platforms:
| Authentication Type | Description |
|---|---|
payment-transaction | Used for a single exchange of goods (e.g. E-commerce checkout) |
recurring-transaction | Used for on-going subscriptions where the billed amount is always the same. (e.g. Monthly Subscription) |
installment-transaction | Used for breaking a single purchase into multiple installments. (e.g. Buy Now Pay Later) |
If your use case does not align with these options, please contact us at support@basistheory.com to discuss what you should select.
Merchant Information
Merchant information allows the issuer and 3DS server to accurately identify your business during authentication. Correctly configuring these details is essential to prevent authentication failures.
Below is a detailed explanation of each merchant information field and guidelines for populating them.
You should have received these values from your Payment Service Provider (PSP) during the 3DS Setup process. If you have questions or haven't received the values for these fields, please contact your PSP and let them know that you need these details for each card network/brand you accept - a request template can be found here.
| Field | Description |
|---|---|
mid | A unique identifier assigned by your PSP to identify your merchant account. |
acquirer_bin | Unique numeric identifier assigned to the PSP by each card network/brand. |
name | The official business name associated with your PSP merchant account. |
country_code | Numeric code representing merchant's country location (ISO 3166-1 standard). |
category_code | A four-digit number assigned by your PSP indicating your type of business. Also known as MCC. |
url | Business URL for the merchant. |
Merchant information often varies depending on the card network or brand. You must implement logic within your application to dynamically select the appropriate values based on the card network or brand used for each transaction.
Merchant Information Example Object
"merchant_info": {
"mid": "9876543210001",
"acquirer_bin": "000000999",
"name": "Example 3DS Merchant",
"country_code": "826",
"category_code": "5999",
"url": "https://example.com",
}
Requestor Information for AMEX, Discover, Cartes Bancaires
American Express, Discover, and Cartes Bancaires (all supported by Basis Theory 3DS) require special 3DS Requestor information in addition to the standard Merchant Information.
If you accept payments from these networks, you should have received the necessary 3DS Requestor Information from your Payment Service Provider (PSP), as outlined in our 3DS Setup Guide.
If any details are missing or unclear, please contact your PSP directly, clearly communicating the specific data points required according to the setup guide - a request template can be found here.
Requestor Information for American Express (AMEX)
| Field | Description |
|---|---|
amex_requestor_type | Defines the merchant type according to American Express requirements. For most cases, we recommend using MER, which represents a general merchant. If your business operates as an aggregator, an online travel agency, or you're unsure whether MER applies to your scenario, refer to the 3DS Setup Guide for alternative values and guidance. |
Requestor Information for Cartes Bancaires
| Field | Description |
|---|---|
cb_siret_number | 14-digit SIRET code identifying a business establishment in France. Used to auto-compute requestor_info.id for standard CB scenarios. |
3DS Requestor ID (requestor_info.id): For CB acquirer + card wallet payment authentications, the requestor ID must combine the SIRET number with the Identifiant Wallet (both provided by Cartes Bancaires) — supply requestor_info.id directly in this case. For non-wallet scenarios, Basis Theory auto-computes requestor_info.id from cb_siret_number.
3DS Requestor Name (requestor_info.name): Must be 40 characters or fewer. This name is displayed on the issuer's ACS challenge interface. Cartes Bancaires recommends a clean, recognizable merchant name to reassure cardholders and maximize conversion.
Acquirer Credentials: The merchant_info.acquirer_bin and merchant_info.mid values must be accepted by the Cartes Bancaires Directory Server. Confirm these values with your CB acquirer directly, or contact Cartes Bancaires.
Supported 3DS Versions: Cartes Bancaires supports 3DS message versions 2.1.0 and 2.2.0.
Requestor Information for Discover
Discover cards, predominantly issued by U.S. banks, rarely require Strong Customer Authentication (SCA).
If you need support for Discover 3DS authentication, please reach out via support@basistheory.com.
Requestor Information Example Object
To simplify implementations - these requestor fields may be included in every request regardless of the brand. Basis Theory automatically handles these values internally, using only those relevant to the card network involved in each transaction.
"requestor_info": {
"amex_requestor_type": "MER", // if American Express is accepted
"cb_siret_number": "78467169500087", // If Cartes Bancaires is accepted, replace with your real SIRET number
}
Purchase Information
It is essential to provide accurate details about the transaction to ensure successful authentication; if you believe you do not need to provide these values, please reach out.
The information below is recommended for all 3DS authentications regardless of the Authentication Type selected.
| Field | Description |
|---|---|
amount | The purchase amount without any punctuation (i.e. 80000 for 800.00). |
currency | The purchase currency code in ISO 4127 standard. |
exponent | The minor units of currency as in the ISO 4127 standard. |
date | The purchase date in UTC timezone and YYYYMMDDhhmmss format. |
transaction_type | Specifies the type of transaction being authenticated. For most use cases, we recommend using purchase. If your transaction falls under special scenarios, such as check acceptance or other uncommon types, refer to our API Reference for additional values and their descriptions. |
Recurring Purchase
The following properties are required to be provided if your Authentication Type is set to recurring-transaction
Beyond the initial CIT authentication - Most recurring transactions or installments are exempt from 3DS authentication. However, the best practice is to verify with your PSP whether additional authentication exemptions or requirements apply to your recurring or installment transactions.
| Field | Description |
|---|---|
recurring_expiration | Final recurring authorization date in YYYYMMDD format. |
recurring_frequency | Number of days between recurring charges. |
Installment Purchase
The following property is required to be provided if your Authentication Type is set to installment-transaction
Beyond the initial CIT authentication - Most recurring transactions or installments are exempt from 3DS authentication. However, the best practice is to verify with your PSP whether additional authentication exemptions or requirements apply to your recurring or installment transactions.
| Field | Description |
|---|---|
installment_count | Total number of installments for installment payments. |
Purchase Information Example Object
"purchase_info": {
"amount": "80000",
"currency": "826",
"exponent": "2",
"date": "20250101141010"
"transaction_type": "purchase",
// only for installment transactions
"installment_count": "5",
// only for recurring transactions
"recurring_expiration": "20250131",
"recurring_frequency": "30",
}
Cardholder Information
Card networks strongly recommend including detailed cardholder information alongside transaction details to improve the likelihood of frictionless authentication.
As a general rule, providing more comprehensive information increases the effectiveness of the 3DS authentication process.
The fields below are, at a minimum, what most card networks require:
| Field | Description |
|---|---|
name | The full name of the cardholder full name. |
email | The email address on-file for the cardholder |
Cardholder Information Example Object
"cardholder_info": {
"name": "John Doe",
"email": "john@example.com",
}
(Optional) Challenge Preference
For the challenge_preference field, the two most common values are available to instruct the issuer of the authentication preference. The table below outlines the common scenarios for most merchants and platforms:
Note: This preference is treated as a suggestion to the issuer and there is no guarantee it will be honored, as the final decision remains at the issuer's discretion.
| Challenge Preference | Description |
|---|---|
no-challenge | Used when preference for a frictionless authentication, to minimize friction by avoiding authentication challenges. |
challenge-requested | Used when explicitly requiring a challenge for the transaction - typically used if you suspect fraud. |
To see more challenge preference options and related EMV codes, refer to our API Reference.
(Optional) Card Brand Selection
For co-badged cards (cards associated with multiple card networks), you can specify which card brand to use for authentication:
| Field | Description |
|---|---|
card_brand | The specific card brand to use for authentication. Should be one of the brands returned in the additional_card_brands property when creating the session. |
Selecting a specific brand allows you to route the authentication through the preferred network, which may be beneficial for:
- Regional regulatory compliance (such as European PSD2 requirements)
- Cost optimization by choosing the most economical network for a transaction
- Customer preference for specific brand loyalty or rewards programs
If card_brand is not specified for a co-badged card, the default primary brand (returned in the card_brand property on session response) will be used.
Populating the Callback URLs
The callback_urls parameter is a required JSON object that specifies where to redirect customers after the 3DS authentication process completes. When creating a session with redirection, you must provide both a success and failure URL:
success: The URL where customers will be redirected after successful authenticationfailure: The URL where customers will be redirected if authentication fails for any reason
These URLs should point to pages in your application that can appropriately handle the authentication outcome and continue the customer's journey.
{
"success": "https://example.com/success",
"failure": "https://example.com/failure"
}
(Optional) Removing Basis Theory Branding
If you want to remove the Basis Theory branding, including logos and text, from the authentication page, you can set the branding.hide_basis_theory_branding parameter in the callback_urls object to true.
{
"success": "https://example.com/success",
"failure": "https://example.com/failure",
"branding": {
"hide_basis_theory_branding": true
}
}
Redirecting to the Authentication Page
After successfully creating a 3DS session (as shown in the previous section), you'll receive a response containing a redirect_url.
This URL is used to redirect the customer to the Basis Theory 3DS authentication page where they will complete the authentication process.
https://api.basistheory.com/3ds/redirect/<SESSION_ID>?apiKey=<PUBLIC_API_KEY>
Where:
SESSION_IDis the unique identifier of the 3DS session you createdapiKeyquery parameter should be set to yourPUBLIC_API_KEY
Implementation Approaches
The redirect_url must open on its own top-level surface. Use a full-page navigation on mobile, and a dedicated 400x600 popup on desktop with a full-page fallback when the popup is blocked. The two options below cover both cases.
Do not load the redirect_url in an iframe or nest it inside an existing checkout modal. The issuer controls how the challenge is presented and completed, so neither you nor Basis Theory can resize or restyle it. Confining it to a smaller or nested viewport clips the challenge and leaves customers unsure which layer to interact with. The telltale symptom is a doubled-modal stack: the issuer's challenge rendered inside an "Authentication Required" panel that sits inside your own checkout modal.
If you want the challenge to render inside your own checkout page instead of on a separate surface, use the SDK embedded CIT integration. That is a different integration built for in-page rendering; the redirect flow described here is not.
You have two main options for handling the redirect:
Option 1: Full Page Redirect (Recommended for Mobile)
For mobile devices or when you want a full-page experience:
// Redirect the current page to the 3DS authentication page
window.location.href = redirectUrl;
Option 2: Popup Window (Recommended for Desktop)
For desktop experiences where you want to keep the checkout page in the background:
// display configuration for the popup window
const popupConfig = {
width: 400,
height: 600,
scrollbars: 'yes',
resizable: 'yes',
toolbar: 'no',
menubar: 'no',
location: 'no',
directories: 'no',
status: 'no'
};
// calculate center position
const left = (screen.width - popupConfig.width) / 2;
const top = (screen.height - popupConfig.height) / 2;
const popupFeatures = `width=${popupConfig.width},height=${popupConfig.height},left=${left},top=${top},${Object.entries(popupConfig).filter(([key]) => key !== 'width' && key !== 'height').map(([key, value]) => `${key}=${value}`).join(',')}`;
try {
// open the popup window
const popup = window.open(redirectUrl, '3ds-authentication', popupFeatures);
// check if popup was blocked
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
// fallback to full page redirect if popup is blocked
alert('Popup blocked. Redirecting to authentication page...');
window.location.href = redirectUrl;
return;
}
// optional: monitor popup status
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
// Handle popup closure - you might want to check session status here
console.log('3DS popup was closed');
}
}, 1000);
} catch (error) {
console.error('Error opening popup:', error);
// Fallback to full page redirect
window.location.href = redirectUrl;
}
Authentication Flow
When customers are redirected to the Basis Theory 3DS authentication page:
- Initial Loading: The page loads and displays the authentication interface
- Authentication Process: Customers complete any required challenges (OTP, biometric, etc.)
- Result Processing: The authentication result is processed
- Success:
- Failure:
- Callback Redirect: Customers are redirected to either your
successorfailureURL
Handling Authentication Completion
After the authentication process completes, customers will be redirected to one of the callback URLs you specified when creating the session:
- Success URL:
callback_urls.success- When authentication is successful - Failure URL:
callback_urls.failure- When authentication fails
Best Practices
User Experience
- Loading States: Show loading indicators while redirecting
- Clear Communication: Inform users they'll be redirected for authentication
- Fallback Options: Always provide fallback to full-page redirect if popups fail
Security Considerations
- Popup Blockers: Consider popup blocker scenarios
- URL Validation: Ensure redirect URLs are properly formatted
- Timeout Handling: Implement timeouts for long-running authentications
Mobile Optimization
- Responsive Design: Use full-page redirects on mobile devices
- Deep Linking: Ensure proper handling of app-to-browser transitions
Error Handling
- Network Issues: Handle network connectivity problems gracefully
- Invalid Sessions: Provide clear error messages for invalid or expired sessions
- Browser Compatibility: Test across different browsers and devices
Example: Complete Implementation
Here's a complete example that handles both popup and redirect scenarios:
function handleThreeDSRedirect(redirectUrl) {
// detect if user is on mobile device
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
// use full page redirect for mobile
window.location.href = redirectUrl;
} else {
// try popup first for desktop
try {
const popup = window.open(
redirectUrl,
'3ds-authentication',
'width=400,height=600,scrollbars=yes,resizable=yes,toolbar=no,menubar=no,location=no,directories=no,status=no'
);
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
// fallback to full page redirect
window.location.href = redirectUrl;
}
} catch (error) {
console.error('Error opening popup:', error);
window.location.href = redirectUrl;
}
}
}
// usage after session creation
handleThreeDSRedirect(sessionResponse.redirect_url);
Retrieving the 3DS Session
After the customer completes the authentication process and is redirected to one of your callback URLs, you need to retrieve the session to verify the authentication results and obtain the final authentication values needed for payment processing.
When to Retrieve the Session
You should retrieve the session:
- After receiving a callback redirect - When the customer is redirected to your success or failure URL
- Before processing payment - To get the authentication values (CAVV, ECI, etc.) for your payment processor
- For verification - To confirm the authentication status and any challenge results
Retrieval Methods
You have two options for getting the session results:
Option 1: Direct API Retrieval
Make a GET request to retrieve the session immediately after receiving the callback redirect:
- cURL
- Node
- .NET
- Python
- Go
curl "https://api.test.basistheory.com/3ds/sessions/<SESSION_ID>" \
-H "BT-API-KEY: <PRIVATE_API_KEY>"
In this example, we are using Basis Theory SDK and Express framework for Node.js.
const express = require("express");
const { BasisTheoryClient } = require("@basis-theory/node-sdk")
const app = express();
const PORT = 3000;
app.use(express.json());
let bt;
(async () => {
// initialize the SDK
bt = await new BasisTheoryClient({ apiKey: "<PRIVATE_API_KEY>", environment: BasisTheoryEnvironment.Test });
// start the server (after SDK is initialized so we don't drop any requests
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
})();
app.post("/create-session", async (req, res) => {
try {
const session = await bt.threeds.sessions.create({
tokenId: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type: "customer",
device: "browser",
authenticationRequest: {
authenticationCategory: "payment",
authenticationType: "payment-transaction",
challengePreference: "no-challenge",
// cardBrand: "Visa", // optional - used if co-badged card
merchantInfo: {
mid: "9876543210001",
acquirerBin: "000000999",
name: "Example 3DS Merchant",
categoryCode: "5999",
countryCode: "826",
url: "https://example.com",
},
requestorInfo: { // if accepting American Express, Discover or Cartes Bancaires
amexRequestorType: "MER", // if American Express is accepted
cbSiretNumber: "78467169500087", // if Cartes Bancaires is accepted
},
purchaseInfo: {
amount: "80000",
currency: "826",
exponent: "2",
date: "20240109141010",
transactionType: "purchase",
//installmentCount": "5", // only for installment transactions
//recurringExpiration: "20250131", // only for recurring transactions
//recurringFrequency: "30", // only for recurring transactions
},
cardholderInfo: {
name: "John Doe",
email: "john@example.com"
}
},
callbackUrls: {
success: "https://example.com/success",
failure: "https://example.com/failure"
}
});
res.status(200).send(session);
} catch (error) {
console.error('Error during session creation:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});
app.get("/:sessionId", async (req, res) => {
const { sessionId } = req.params;
try {
const session = await bt.threeds.sessions.get(sessionId);
if (!session) {
return res.status(500).send({ error: "Failed to get session." });
}
res.status(200).send(session);
} catch (error) {
console.error('Error during session retrieval:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});
In this example, we are using Basis Theory SDK and ASP.NET Core Framework.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using BasisTheory.Client;
using BasisTheory.Client.Threeds;
namespace server.Controllers
{
public class Program
{
public static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://0.0.0.0:4242")
.UseWebRoot("public")
.UseStartup<Startup>()
.Build()
.Run();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddNewtonsoftJson();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseStaticFiles();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
[ApiController]
public class ThreeDsApiController : Controller
{
private readonly BasisTheory _client;
public ThreeDsApiController()
{
_client = new BasisTheory(apiKey: "<PRIVATE_API_KEY>", clientOptions: new ClientOptions
{
BaseUrl = BasisTheoryEnvironment.Test
});
}
[HttpPost("create-session")]
public async Task<ActionResult> CreateSession()
{
try
{
var session = await _client.Threeds.Sessions.CreateAsync(new CreateThreeDsSessionRequest
{
TokenId = "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
Type = "customer",
Device = "browser",
AuthenticationRequest = new AuthenticateThreeDsSessionRequest
{
AuthenticationCategory = "payment",
AuthenticationType = "payment-transaction",
ChallengePreference = "no-challenge",
// CardBrand = "Visa", // optional - used if co-badged card
MerchantInfo = new ThreeDsMerchantInfo
{
Mid = "9876543210001",
AcquirerBin = "000000999",
Name = "Example 3DS Merchant",
CategoryCode = "5999",
CountryCode = "826",
Url = "https://example.com"
},
RequestorInfo = new ThreeDsRequestorInfo // if accepting American Express, Discover or Cartes Bancaires
{
AmexRequestorType = "MER", // if American Express is accepted
CbSiretNumber = "78467169500087", // if Cartes Bancaires is accepted
},
PurchaseInfo = new ThreeDsPurchaseInfo
{
Amount = "80000",
Currency = "826",
Exponent = "2",
Date = "20240109141010",
TransactionType = "purchase",
// InstallmentCount = "5", // only for installment transactions
// RecurringExpiration = "20250131", // only for recurring transactions
// RecurringFrequency = "30", // only for recurring transactions
},
CardholderInfo = new ThreeDsCardholderInfo
{
Name = "John Doe",
Email = "john@example.com"
}
},
CallbackUrls = new ThreeDsCallbackUrls
{
Success = "https://example.com/success",
Failure = "https://example.com/failure"
}
});
if (session == null)
{
return Problem("Error during session creation.");
}
return Ok(session);
}
catch (Exception ex)
{
Console.WriteLine($"Error during session creation: {ex.Message}");
return Problem("Internal Server Error");
}
}
[HttpGet("{sessionId:guid}")]
public async Task<ActionResult> GetSession([FromRoute] Guid sessionId)
{
try
{
var session = await _client.Threeds.Sessions.GetAsync(sessionId.ToString());
if (session == null)
{
return Problem("Failed to get session.");
}
return Ok(session);
}
catch (Exception ex)
{
Console.WriteLine($"Error during session retrieval: {ex.Message}");
return Problem("Internal Server Error");
}
}
}
}
In this example, we are using Basis Theory SDK and Flask Framework.
import os
from flask import Flask, request, jsonify
from basis_theory import (
BasisTheory,
ThreeDsMerchantInfo,
ThreeDsRequestorInfo,
ThreeDsPurchaseInfo,
ThreeDsCardholderInfo,
AuthenticateThreeDsSessionRequest,
ThreeDsCallbackUrls
)
app = Flask(__name__)
client = BasisTheory(api_key="<PRIVATE_API_KEY>", environment=BasisTheoryEnvironment.TEST)
@app.route('/create-session', methods=['POST'])
def create_session():
session = client.threeds.sessions.create(
token_id="d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type="customer",
device="browser",
authentication_request=AuthenticateThreeDsSessionRequest(
authentication_category="payment",
authentication_type="payment-transaction",
challenge_preference="no-challenge",
# card_brand="Visa", # optional - used if co-badged card
merchant_info=ThreeDsMerchantInfo(
mid="9876543210001",
acquirer_bin="000000999",
name="Example 3DS Merchant",
category_code="5999",
country_code="826",
url="https://example.com"
),
requestor_info=ThreeDsRequestorInfo( # if accepting American Express, Discover or Cartes Bancaires
amex_requestor_type="MER", # if American Express is accepted
cb_siret_number="78467169500087" # if Cartes Bancaires is accepted
),
purchase_info=ThreeDsPurchaseInfo(
amount="80000",
currency="826",
exponent="2",
date="20240109141010",
transaction_type="purchase",
# installment_count="5", # only for installment transactions
# recurring_expiration="20250131", # only for recurring transactions
# recurring_frequency="30", # only for recurring transactions
),
cardholder_info=ThreeDsCardholderInfo(
name="John Doe",
email="john@example.com"
),
),
callback_urls=ThreeDsCallbackUrls(
success="https://example.com/success",
failure="https://example.com/failure"
)
)
if session is None:
return jsonify({"error": "Error during session creation."}), 500
return jsonify(session.to_dict()), 200
@app.route('/<sessionId>', methods=['GET'])
def get_session(sessionId):
try:
session = client.threeds.sessions.get(sessionId)
if session is None:
return jsonify({"error": "Failed to get session."}), 500
return jsonify(session.to_dict()), 200
except Exception as ex:
print(f"Error during session retrieval: {ex}")
return jsonify({"error": "Internal Server Error"}), 500
if __name__ == '__main__':
app.run(port=4242, debug=True)
In this example, we are using Basis Theory SDK, Go HTTP package and the Gorilla Mux Router.
package main
import (
"context"
"encoding/json"
"github.com/Basis-Theory/go-sdk"
basistheoryclient "github.com/Basis-Theory/go-sdk/client"
"github.com/Basis-Theory/go-sdk/option"
"github.com/Basis-Theory/go-sdk/threeds"
"github.com/gorilla/mux"
"log"
"net/http"
)
var client *basistheoryclient.Client
func main() {
client := basistheoryclient.NewClient(
option.WithAPIKey("<API_KEY>"),
)
router := mux.NewRouter()
router.HandleFunc("/create-session", createSession).Methods("POST")
router.HandleFunc("/{sessionId}", getSession).Methods("GET")
addr := "localhost:4242"
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, router))
}
func pointerToString(s string) *string {
return &s
}
func createSession(rw http.ResponseWriter, r *http.Request) {
session := client.Threeds.Sessions.Create(context.Background(), &threeds.CreateThreeDsSessionRequest{
TokenId: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
Type: "customer",
Device: "browser",
CallbackUrls: &threeds.ThreeDsCallbackUrls{
Success: pointerToString("https://example.com/success"),
Failure: pointerToString("https://example.com/failure"),
},
})
authRequest := &threeds.AuthenticateThreeDsSessionRequest{
AuthenticationCategory: pointerToString("payment"),
AuthenticationType: pointerToString("payment-transaction"),
ChallengePreference: pointerToString("no-challenge"),
// CardBrand: pointerToString("Visa"), // optional - used if co-badged card
}
merchantInfo := &basistheory.ThreeDsMerchantInfo{
Mid: pointerToString("9876543210001"),
AcquirerBin: pointerToString("000000999"),
Name: pointerToString("Example 3DS Merchant"),
CategoryCode: pointerToString("5999"),
CountryCode: pointerToString("826"),
Url: pointerToString("https://example.com"),
}
requestorInfo := &basistheory.ThreeDsRequestorInfo{ // if accepting American Express, Discover or Cartes Bancaires
AmexRequestorType: pointerToString("MER"), // if American Express is accepted
CbSiretNumber: pointerToString("78467169500087"), // if Cartes Bancaires is accepted
}
purchaseInfo := &basistheory.ThreeDsPurchaseInfo{
Amount: pointerToString("80000"),
Currency: pointerToString("826"),
Exponent: pointerToString("2"),
Date: pointerToString("20240109141010"),
TransactionType: pointerToString("purchase"),
// InstallmentCount: pointerToString("5"), // only for installment transactions
// RecurringExpiration: pointerToString("20250131"), // only for recurring transactions
// RecurringFrequency: pointerToString("30"), // only for recurring transactions
}
cardholderInfo := &basistheory.ThreeDsCardholderInfo{
Name: pointerToString("John Doe"),
Email: pointerToString("john@example.com"),
}
authRequest.MerchantInfo = merchantInfo
authRequest.RequestorInfo = requestorInfo
authRequest.PurchaseInfo = purchaseInfo
authRequest.CardholderInfo = cardholderInfo
session.AuthenticationRequest = authRequest
createResponse, createErr := client.Threeds.Sessions.Create(context.Background(), session)
if createErr != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": createErr.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(createResponse)
}
func getSession(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionId := vars["sessionId"]
session, err := client.Threeds.Sessions.Get(context.Background(), sessionId)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": err.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(session)
}
Option 2: Webhook Notifications
Alternatively, you can subscribe to webhook events to be notified when authentication completes:
3ds.session.authenticated- Triggered when authentication finishes3ds.session.challenge-completed- Triggered when a challenge is completed
The webhook events will not include the full authentication object, but you can use the session_id to retrieve the full session and get the authentication values.
The main benefit of using webhooks is that the webhook events include the authentication_status value, which can be used to determine if the authentication was successful or not.
This is useful if you want to handle different scenarios based on the authentication status.
Verifying the Authentication Results
The retrieved session contains an authentication object with everything you need to validate a 3DS transaction or identify why it failed. The code examples above show how to retrieve this data - now let's examine what to do with it.
See below a detailed table with descriptions for what each field means, and continue reading to understand different authentication scenarios that can happen with a 3DS transaction.
| Field | Description |
|---|---|
session_id | The id for the authenticated 3DS session |
threeds_version | The 3DS version (i.e. 2.2.0) used in the transaction |
token_id | The ID of the card or network_token token used in the 3DS transaction |
token_intent_id | The ID of the card or network_token token intent used in the 3DS transaction |
acs_transaction_id | The transaction ID from the 3DS Access Control Server (ACS) |
ds_transaction_id | The transaction ID from the 3DS Directory Server (DS) |
acs_reference_number | A unique identifier assigned to the DS by EMVCo |
ds_reference_number | A unique identifier assigned to the ACS by EMVCo |
authentication_value | The 3DS cryptogram value used to authorize the transaction. Also know as CAVV, AAV, AEVV, etc. |
authentication_status | The outcome of the 3DS authentication. See Authentication Scenarios |
authentication_status_code | EMVCo character code for the authentication status |
authentication_status_reason | Additional information about the authentication status if necessary. See Failed Authentication. Not provided on frictionless authentication |
authentication_status_reason_code | EMVCo numeric code for the authentication status reason. |
directory_status_code | EMVCo character code for the directory status |
eci | Electronic Commerce Indicator (ECI) |
acs_challenge_mandated | Indicates whether regional mandates (e.g., PSD2) require a challenge to be performed |
authentication_challenge_type | The type of challenge authentication used (if challenge) |
acs_challenge_url | The URL to be used for the challenge |
challenge_preference | The selected Challenge Preference during authentication |
challenge_preference_code | EMVCo numeric code for the selected challenge preference |
challenge_attempts | The number of challenges attempted by the cardholder. Not provided on frictionless authentication |
challenge_cancel_reason | The reason why a challenge was cancelled. Not provided on frictionless authentication |
challenge_cancel_reason_code | EMVCo numeric code for the challenge cancel reason. Not provided on frictionless authentication |
cardholder_info | Unspecified information from the issuer to be displayed to the cardholder |
whitelist_status | Indicates if the cardholder whitelisted the merchant |
whitelist_status_source | Identifies the system that set the whitelist value |
message_extensions | Array of Message Extensions - Data necessary to support requirements not defined in the standard 3DS format |
Authentication Scenarios
Below are how to handle the scenarios that could be returned from the Authentication step. Generally, this will fall into he following scenarios:
| Authentication Scenarios | Directory Status Code | Description |
|---|---|---|
| Frictionless | Y, A | The authentication was approved by the issuer and the transaction may proceed. |
| Challenge | C | The issuer is requesting additional authentication before allowing the transaction to proceed. |
| Decoupled Challenge | D | The issuer requires additional authentication to be performed outside of the transaction scope. |
| 3DS Failed | N, U, R | The 3DS authentication was not approved. Reason is provided in authentication_status_reason property. |
Frictionless Authentication
If the authentication_status indicates success, and the directory_status_code indicates Y or A, your authentication has been approved, and the response includes the authentication values needed to send to your Processor.
Note: The attempted status also produces a frictionless authentication. However, in this case, it indicates the ACS (Access Control Server) encountered an issue authenticating the transaction. From the merchant's perspective, it works like a success (no further authentication steps are required), although liability considerations may differ.
Challenge Authentication
If the directory_status_code indicates the code C, the cardholder completed an additional verification step inside the Basis Theory 3DS Authentication page—a "Challenge"—before the 3DS authentication could be finalized and the final authentication value (CAVV) provided.
Alternatively, you can use our Webhooks solution. Subscribing a webhook to the 3ds.session.challenge-completed event will trigger when a challenge completion has finished and contain the id to enable Getting a Challenge Result.
Challenges can vary significantly, often involving methods such as entering a one-time password (OTP) or authorizing the transaction via a banking app. The presentation and completion method of the challenge are determined entirely by the issuer, and the merchant has no control over this process.
Getting a Challenge Result
Once a challenge is completed, you must retrieve the result directly from the API. This step is essential to determining the challenge's outcome (success or failure) and obtaining the necessary authentication values (e.g., CAVV, ECI) for successfully authenticated transactions.
- cURL
- Node
- .NET
- Python
- Go
curl "https://api.test.basistheory.com/3ds/sessions/<SESSION_ID>/challenge-result" \
-H "BT-API-KEY: <PRIVATE_API_KEY>"
In this example, we are using Basis Theory SDK and Express framework for Node.js.
const express = require("express");
const { BasisTheoryClient } = require("@basis-theory/node-sdk")
const app = express();
const PORT = 3000;
app.use(express.json());
let bt;
(async () => {
// initialize the SDK
bt = await new BasisTheoryClient({ apiKey: "<PRIVATE_API_KEY>", environment: BasisTheoryEnvironment.Test });
// start the server (after SDK is initialized so we don't drop any requests
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
})();
app.post("/create-session", async (req, res) => {
try {
const session = await bt.threeds.sessions.create({
tokenId: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type: "customer",
device: "browser",
authenticationRequest: {
authenticationCategory: "payment",
authenticationType: "payment-transaction",
challengePreference: "no-challenge",
// cardBrand: "Visa", // optional - used if co-badged card
merchantInfo: {
mid: "9876543210001",
acquirerBin: "000000999",
name: "Example 3DS Merchant",
categoryCode: "5999",
countryCode: "826",
url: "https://example.com",
},
requestorInfo: { // if accepting American Express, Discover or Cartes Bancaires
amexRequestorType: "MER", // if American Express is accepted
cbSiretNumber: "78467169500087", // if Cartes Bancaires is accepted
},
purchaseInfo: {
amount: "80000",
currency: "826",
exponent: "2",
date: "20240109141010",
transactionType: "purchase",
//installmentCount: "5", // only for installment transactions
//recurringExpiration: "20250131", // only for recurring transactions
//recurringFrequency: "30", // only for recurring transactions
},
cardholderInfo: {
name: "John Doe",
email: "john@example.com"
}
},
callbackUrls: {
success: "https://example.com/success",
failure: "https://example.com/failure"
}
});
res.status(200).send(session);
} catch (error) {
console.error('Error during session creation:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});
app.get("/:sessionId", async (req, res) => {
const { sessionId } = req.params;
try {
const session = await bt.threeds.sessions.get(sessionId);
if (!session) {
return res.status(500).send({ error: "Failed to get session." });
}
res.status(200).send(session);
} catch (error) {
console.error('Error during session retrieval:', 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.sessions.getChallengeResult(sessionId);
if (!result) {
return res.status(500).send({ error: "Failed to get challenge result." });
}
res.status(200).send(result);
} catch (error) {
console.error('Error getting challenge result:', error);
res.status(500).send({ error: "Internal Server Error" });
}
});
In this example, we are using Basis Theory SDK and ASP.NET Core Framework.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using BasisTheory.Client;
using BasisTheory.Client.Threeds;
namespace server.Controllers
{
public class Program
{
public static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://0.0.0.0:4242")
.UseWebRoot("public")
.UseStartup<Startup>()
.Build()
.Run();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddNewtonsoftJson();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseStaticFiles();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
[ApiController]
public class ThreeDsApiController : Controller
{
private readonly BasisTheory _client;
public ThreeDsApiController()
{
_client = new BasisTheory(apiKey: "<PRIVATE_API_KEY>", clientOptions: new ClientOptions
{
BaseUrl = BasisTheoryEnvironment.Test
});
}
[HttpPost("create-session")]
public async Task<ActionResult> CreateSession()
{
try
{
var session = await _client.Threeds.Sessions.CreateAsync(new CreateThreeDsSessionRequest
{
TokenId = "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
Type = "customer",
Device = "browser",
AuthenticationRequest = new AuthenticateThreeDsSessionRequest
{
AuthenticationCategory = "payment",
AuthenticationType = "payment-transaction",
ChallengePreference = "no-challenge",
// CardBrand = "Visa", // optional - used if co-badged card
MerchantInfo = new ThreeDsMerchantInfo
{
Mid = "9876543210001",
AcquirerBin = "000000999",
Name = "Example 3DS Merchant",
CategoryCode = "5999",
CountryCode = "826",
Url = "https://example.com"
},
RequestorInfo = new ThreeDsRequestorInfo // if accepting American Express, Discover or Cartes Bancaires
{
AmexRequestorType = "MER", // if American Express is accepted
CbSiretNumber = "78467169500087", // if Cartes Bancaires is accepted
},
PurchaseInfo = new ThreeDsPurchaseInfo
{
Amount = "80000",
Currency = "826",
Exponent = "2",
Date = "20240109141010",
TransactionType = "purchase",
// InstallmentCount = "5", // only for installment transactions
// RecurringExpiration = "20250131", // only for recurring transactions
// RecurringFrequency = "30", // only for recurring transactions
},
CardholderInfo = new ThreeDsCardholderInfo
{
Name = "John Doe",
Email = "john@example.com"
}
},
CallbackUrls = new ThreeDsCallbackUrls
{
Success = "https://example.com/success",
Failure = "https://example.com/failure"
}
});
if (session == null)
{
return Problem("Error during session creation.");
}
return Ok(session);
}
catch (Exception ex)
{
Console.WriteLine($"Error during session creation: {ex.Message}");
return Problem("Internal Server Error");
}
}
[HttpGet("{sessionId:guid}")]
public async Task<ActionResult> GetSession([FromRoute] Guid sessionId)
{
try
{
var session = await _client.Threeds.Sessions.GetAsync(sessionId.ToString());
if (session == null)
{
return Problem("Failed to get session.");
}
return Ok(session);
}
catch (Exception ex)
{
Console.WriteLine($"Error during session retrieval: {ex.Message}");
return Problem("Internal Server Error");
}
}
[HttpGet("{sessionId:guid}/challenge-result")]
public async Task<ActionResult> GetChallengeResult([FromRoute] Guid sessionId)
{
try {
var result = await _client.Threeds.Sessions.GetChallengeResultAsync(sessionId.ToString());
if (result == null)
{
return Problem("Failed to get challenge result.");
}
return Ok(result);
}
catch (Exception ex)
{
Console.WriteLine($"Error getting challenge result: {ex.Message}");
return Problem("Internal Server Error");
}
}
}
}
In this example, we are using Basis Theory SDK and Flask Framework.
import os
from flask import Flask, request, jsonify
from basis_theory import (
BasisTheory,
ThreeDsMerchantInfo,
ThreeDsRequestorInfo,
ThreeDsPurchaseInfo,
ThreeDsCardholderInfo,
AuthenticateThreeDsSessionRequest,
ThreeDsCallbackUrls
)
app = Flask(__name__)
client = BasisTheory(api_key="<PRIVATE_API_KEY>", environment=BasisTheoryEnvironment.TEST)
@app.route('/create-session', methods=['POST'])
def create_session():
session = client.threeds.sessions.create(
token_id="d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
type="customer",
device="browser",
authentication_request=AuthenticateThreeDsSessionRequest(
authentication_category="payment",
authentication_type="payment-transaction",
challenge_preference="no-challenge",
# card_brand="Visa", # optional - used if co-badged card
merchant_info=ThreeDsMerchantInfo(
mid="9876543210001",
acquirer_bin="000000999",
name="Example 3DS Merchant",
category_code="5999",
country_code="826",
url="https://example.com"
),
requestor_info=ThreeDsRequestorInfo( # if accepting American Express, Discover or Cartes Bancaires
amex_requestor_type="MER", # if American Express is accepted
cb_siret_number="78467169500087" # if Cartes Bancaires is accepted
),
purchase_info=ThreeDsPurchaseInfo(
amount="80000",
currency="826",
exponent="2",
date="20240109141010",
transaction_type="purchase",
# installment_count="5", # only for installment transactions
# recurring_expiration="20250131", # only for recurring transactions
# recurring_frequency="30", # only for recurring transactions
),
cardholder_info=ThreeDsCardholderInfo(
name="John Doe",
email="john@example.com"
),
),
callback_urls=ThreeDsCallbackUrls(
success="https://example.com/success",
failure="https://example.com/failure"
)
)
if session is None:
return jsonify({"error": "Error during session creation."}), 500
return jsonify(session.to_dict()), 200
@app.route('/<sessionId>', methods=['GET'])
def get_session(sessionId):
try:
session = client.threeds.sessions.get(sessionId)
if session is None:
return jsonify({"error": "Failed to get session."}), 500
return jsonify(session.to_dict()), 200
except Exception as ex:
print(f"Error during session retrieval: {ex}")
return jsonify({"error": "Internal Server Error"}), 500
@app.route('/<sessionId>/challenge-result', methods=['GET'])
def get_challenge_result(sessionId):
try:
result = client.threeds.sessions.get_challenge_result(sessionId)
if result is None:
return jsonify({"error": "Failed to get challenge result."}), 500
return jsonify(result.to_dict()), 200
except Exception as ex:
print(f"Error getting challenge result: {ex}")
return jsonify({"error": "Internal Server Error"}), 500
if __name__ == '__main__':
app.run(port=4242, debug=True)
In this example, we are using Basis Theory SDK, Go HTTP package and the Gorilla Mux Router.
package main
import (
"context"
"encoding/json"
"github.com/Basis-Theory/go-sdk"
basistheoryclient "github.com/Basis-Theory/go-sdk/client"
"github.com/Basis-Theory/go-sdk/option"
"github.com/Basis-Theory/go-sdk/threeds"
"github.com/gorilla/mux"
"log"
"net/http"
)
var client *basistheoryclient.Client
func main() {
client := basistheoryclient.NewClient(
option.WithAPIKey("<API_KEY>"),
)
router := mux.NewRouter()
router.HandleFunc("/create-session", createSession).Methods("POST")
router.HandleFunc("/{sessionId}", getSession).Methods("GET")
router.HandleFunc("/{sessionId}/challenge-result", getChallengeResult).Methods("GET")
addr := "localhost:4242"
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, router))
}
func pointerToString(s string) *string {
return &s
}
func createSession(rw http.ResponseWriter, r *http.Request) {
session := client.Threeds.Sessions.Create(context.Background(), &threeds.CreateThreeDsSessionRequest{
TokenId: "d65ead1a-84aa-45eb-96ba-f3e7e26087c0",
Type: "customer",
Device: "browser",
CallbackUrls: &threeds.ThreeDsCallbackUrls{
Success: pointerToString("https://example.com/success"),
Failure: pointerToString("https://example.com/failure"),
},
})
authRequest := &threeds.AuthenticateThreeDsSessionRequest{
AuthenticationCategory: pointerToString("payment"),
AuthenticationType: pointerToString("payment-transaction"),
ChallengePreference: pointerToString("no-challenge"),
// CardBrand: pointerToString("Visa"), // optional - used if co-badged card
}
merchantInfo := &basistheory.ThreeDsMerchantInfo{
Mid: pointerToString("9876543210001"),
AcquirerBin: pointerToString("000000999"),
Name: pointerToString("Example 3DS Merchant"),
CategoryCode: pointerToString("5999"),
CountryCode: pointerToString("826"),
Url: pointerToString("https://example.com"),
}
requestorInfo := &basistheory.ThreeDsRequestorInfo{ // if accepting American Express, Discover or Cartes Bancaires
AmexRequestorType: pointerToString("MER"), // if American Express is accepted
CbSiretNumber: pointerToString("78467169500087"), // if Cartes Bancaires is accepted
}
purchaseInfo := &basistheory.ThreeDsPurchaseInfo{
Amount: pointerToString("80000"),
Currency: pointerToString("826"),
Exponent: pointerToString("2"),
Date: pointerToString("20240109141010"),
TransactionType: pointerToString("purchase"),
// InstallmentCount: pointerToString("5"), // only for installment transactions
// RecurringExpiration: pointerToString("20250131"), // only for recurring transactions
// RecurringFrequency: pointerToString("30"), // only for recurring transactions
}
cardholderInfo := &basistheory.ThreeDsCardholderInfo{
Name: pointerToString("John Doe"),
Email: pointerToString("john@example.com"),
}
authRequest.MerchantInfo = merchantInfo
authRequest.RequestorInfo = requestorInfo
authRequest.PurchaseInfo = purchaseInfo
authRequest.CardholderInfo = cardholderInfo
session.AuthenticationRequest = authRequest
createResponse, createErr := client.Threeds.Sessions.Create(context.Background(), session)
if createErr != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": createErr.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(createResponse)
}
func getSession(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionId := vars["sessionId"]
session, err := client.Threeds.Sessions.Get(context.Background(), sessionId)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": err.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(session)
}
func getChallengeResult(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionId := vars["sessionId"]
result, err := client.Threeds.Sessions.GetChallengeResult(context.Background(), sessionId)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": err.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(result)
}
Decoupled Challenge Authentication
Decoupled Challenge Authentication is a 3DS method that separates cardholder authentication from the payment flow, allowing the issuer to handle authentication without direct cardholder interaction at the time of the transaction.
If the directory_status_code indicates the code D, then that indicates a Decoupled Challenge Authentication.
This approach is more common in Merchant-Initiated Transactions (MIT) but possible in cases where the cardholder's device cannot support a typical challenge flow during a CIT.
Since this authentication occurs outside the immediate transaction flow, you must subscribe to the 3ds.session.decoupled-challenge-notification event via Webhooks to confirm when the authentication is complete.
Failed Authentication
A 3DS authentication may fail for various reasons, ranging from technical issues to rejections triggered by fraud or risk concerns. When you encounter a failed, unavailable, or rejected authentication_status, refer to the authentication_status_reason property to understand why the authentication did not succeed.
Depending on the reason, you can either:
- Retry the 3DS authentication using the same information.
- Adjust the authentication request (e.g., providing more details) before retrying.
The following table outlines possible authentication_status_reason values and recommended actions.
| Authentication Status Reason | Recommended Action |
|---|---|
low-confidencemedium-confidencehigh-confidencevery-high-confidenceauthentication-attempted-but-not-performedidentity-check-insights | The authentication was successful even though it failed, use authentication results returned with payment. |
invalid-transactiontransaction-not-permittedcardholder-not-enrollednon-payment-transaction-not-support3ri-not-supportedvmid-not-eligible-for-requested-programprotocol-version-not-supported-by-acs | Attempt payment without 3DS authentication. |
card-authentication-failedtoo-many-authentication-attemptscard-expiredinvalid-card-numberno-card-recordsecurity-failurestolen-cardsuspected-fraud | Inform consumer to use a different card or double-check details. |
unknown-deviceunsupported-device | Inform consumer to use a different device. |
timeout-at-acsmax-challenges-exceededacs-technical-issuedecoupled-authentication-requireddecoupled-authentication-timeoutinsufficient-decoupled-authentication-timeerror-connecting-to-acsacs-timed-outinvalid-response-from-acssystem-error-response-from-acsinternal-error-while-generating-cavvdevice-3ri-not-routed-to-smart-authentication-stand-intransaction-excluded-from-attempts-processing | Retry authentication. |
Refer to our API documentation for details on what each reason code means specifically.