Authenticate with 3DS
3DS (3D Secure) is an online payment authentication protocol that enhances anti-fraud efforts. It requires cardholders to undergo an additional layer of verification, such as a one-time password or biometric scan, during online transactions. It is also the most common implementation of Secure Customer Authentication (SCA) for regulations such as PSD2.
In this guide, we will set up the Basis Theory 3DS SDK to start a 3DS Customer Initiated Transaction (CIT) in a frontend web application, and securely authenticate it using the Basis Theory Platform. If you are looking to authenticate a Merchant Initiated Transaction (MIT), please refer to the MIT Guide.
Getting Started
To get started, you will need to create a Basis Theory Account and a TEST Tenant.
Provisioning Resources
In this section, we will explore the bare minimum resources necessary to authenticate 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
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
Configuring the Basis Theory 3DS SDK
The 3DS SDK is available as an NPM package for Web and React Native, and also available for Android and iOS apps.
See instructions below on how to install and configure it.
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 Session and Device Fingerprinting
The 3D Secure process starts with identifying certain information about the customer's device to aid in verifying if the transaction is valid. The Basis Theory 3DS SDK takes care of that process, collecting all necessary information and sending that over to the 3D secure server for processing.
First, let's create a 3DS session
and pass the created card token id
as the pan
property.
The SDK will then collect the device info as previously described, and send it alongside the de-tokenized card number (pan) to the 3DS server in order to initiate a 3DS transaction.
- Web
- iOS
- Android
- React Native
import { BasisTheory3ds } from "@basis-theory/3ds-web";
const checkout = async () => {
// initializing the 3ds sdk
const bt3ds = BasisTheory3ds("<PUBLIC_API_KEY>");
// creating the session
const session = await bt3ds.createSession({ pan: "<CARD_TOKEN_ID>" });
}
class ViewController: UIViewController {
private var threeDSService: ThreeDSService! // important to keep `threeDSService` outside of the Task, otherwise swift will handle it as a weak reference and you may experience undesired behavior
let customHeaders: [String: String] = [
"Authorization": "Bearer your_token_here",
"Trace-Id": "your_trace_id_here"
]
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
threeDSService = try ThreeDSService.builder()
.withApiKey("<PUBLIC_API_KEY>")
.withAuthenticationEndpoint("<YOUR AUTHENTICATION ENDPOINT>", customHeaders)
.build()
try await threeDSService.initialize { [weak self] warnings in
if let warnings = warnings, !warnings.isEmpty {
let messages = warnings.map { $0.message }.joined(separator: "\n")
print(messages)
} else {
print("No warnings.")
}
}
// creating the session
let session = try await threeDSService.createSession(tokenId: "<CARD TOKEN ID>")
} catch {
print(error.localizedDescription)
}
}
}
}
val customHeaders = Headers.Builder()
.add("Authorization", "Bearer your_token_here")
.add("Trace-Id", "your_trace_id_here")
.build();
val threeDSService = ThreeDsService.Builder()
.withApiKey("<PUBLIC_API_KEY>")
.withApplicationContext(context)
.withAuthenticationEndpoint("Your 3DS authentication endpoint URL", customHeaders) // https://developers.basistheory.com/docs/guides/process/authenticate-with-3ds#authenticating-a-session
.apply {
// make sure withSandbox is removed in production environments
if (BuildConfig.DEBUG) {
withSandbox()
}
}
.build()
val warnings = threeDSService.initialize()
if (!warnings.isNullOrEmpty()) {
// inspect warnings
}
// creating the session
val session = threeDSService.createSession("<CARD_TOKEN_ID>")
import { BasisTheory3dsProvider, useBasisTheory3ds } from "@basis-theory/3ds-react-native";
const App = () => {
return (
<BasisTheory3dsProvider apiKey={"<PUBLIC_API_KEY>"}>
<MyApp />
</BasisTheory3dsProvider>
);
}
const MyApp = () => {
const { createSession, startChallenge } = useBasisTheory3ds();
//creating the session
const session = await createSession({ tokenId: "<CARD_TOKEN_ID>" });
}
<PUBLIC_API_KEY>
with the Public API Key you created in the Public Application step,
and <CARD_TOKEN_ID>
with the token id created in the Creating a Card Token step.Authenticating a Session
Once the session
is created, it must be authenticated.
In this process, the merchant or requestor 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.
- Node
- .NET
- Python
- Go
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("<API_KEY>");
})();
app.post("/:sessionId/authenticate", async (req, res) => {
const { sessionId } = req.params;
try {
const authentication = await bt.threeds.authenticateSession(sessionId, {
authenticationCategory: "payment",
authenticationType: "payment-transaction",
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"
}
});
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}`);
});
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.net.ThreeDS;
using BasisTheory.net.ThreeDS.Requests;
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 ThreeDSClient _client;
public ThreeDsApiController()
{
_client = new ThreeDSClient("<PRIVATE_API_KEY>");
}
[HttpPost("{sessionId:guid}/authenticate")]
public async Task<ActionResult> AuthenticateSession([FromRoute] Guid sessionId)
{
var authentication = await _client.AuthenticateThreeDSSessionAsync(sessionId.ToString(), new AuthenticateThreeDSSessionRequest
{
AuthenticationCategory = "payment",
AuthenticationType = "payment-transaction",
PurchaseInfo = new ThreeDSPurchaseInfo
{
Amount = "80000",
Currency = "826",
Exponent = "2",
Date = "20240109141010"
},
RequestorInfo = new ThreeDSRequestorInfo
{
Id = "example-3ds-merchant",
Name = "Example 3DS Merchant",
Url = "https://www.example.com/example-merchant"
},
MerchantInfo = new ThreeDSMerchantInfo
{
Mid = "9876543210001",
AcquirerBin = "000000999",
Name = "Example 3DS Merchant",
CategoryCode = "7922",
CountryCode = "826"
},
CardholderInfo = new ThreeDSCardholderInfo
{
Name = "John Doe",
Email = "john@me.com"
}
});
if (authentication == null)
{
return BadRequest("Failed to authenticate session.");
}
return Ok(authentication);
}
}
}
In this example, we are using Basis Theory SDK and Flask Framework.
import os
from flask import Flask, request, jsonify
import basistheory
from basistheory.api import three_ds_api
from basistheory.model import AuthenticateThreeDSSessionRequest, ThreeDSPurchaseInfo, ThreeDSRequestorInfo, ThreeDSMerchantInfo, ThreeDSCardholderInfo
app = Flask(__name__)
@app.route('/<sessionId>/authenticate', methods=['POST'])
def authenticate_session(sessionId):
config = basistheory.Configuration(
host="https://api.basistheory.com",
api_key="<PRIVATE_API_KEY>"
)
with basistheory.ApiClient(configuration=config) as api_client:
three_ds_client = three_ds_api.ThreeDSApi(api_client)
authentication_request = AuthenticateThreeDSSessionRequest(
authentication_category="payment",
authentication_type="payment-transaction",
purchase_info=ThreeDSPurchaseInfo(
amount="80000",
currency="826",
exponent="2",
date="20240109141010"
),
requestor_info=ThreeDSRequestorInfo(
id="example-3ds-merchant",
name="Example 3DS Merchant",
url="https://www.example.com/example-merchant"
),
merchant_info=ThreeDSMerchantInfo(
mid="9876543210001",
acquirer_bin="000000999",
name="Example 3DS Merchant",
category_code="7922",
country_code="826"
),
cardholder_info=ThreeDSCardholderInfo(
name="John Doe",
email="john@me.com"
)
)
authentication = three_ds_client.three_ds_authenticate_session(sessionId, authenticate_three_ds_session_request=authentication_request)
return jsonify(authentication.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"
"log"
"net/http"
"github.com/Basis-Theory/basistheory-go/v5"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/{sessionId}/authenticate", authenticateSession).Methods("POST")
addr := "localhost:4242"
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, router))
}
func authenticateSession(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionId := vars["sessionId"]
configuration := basistheory.NewConfiguration()
apiClient := basistheory.NewAPIClient(configuration)
contextWithAPIKey := context.WithValue(context.Background(), basistheory.ContextAPIKeys, map[string]basistheory.APIKey{
"ApiKey": {Key: "<PRIVATE_API_KEY>"},
})
authenticateThreeDSSessionRequest := *basistheory.NewAuthenticateThreeDSSessionRequest()
authenticateThreeDSSessionRequest.SetAuthenticationCategory("payment")
authenticateThreeDSSessionRequest.SetAuthenticationType("payment-transaction")
purchaseInfo := *basistheory.NewThreeDSPurchaseInfo()
purchaseInfo.SetAmount("80000")
purchaseInfo.SetCurrency("826")
purchaseInfo.SetExponent("2")
purchaseInfo.SetDate("20240109141010")
requestorInfo := *basistheory.NewThreeDSRequestorInfo()
requestorInfo.SetId("example-3ds-merchant")
requestorInfo.SetName("Example 3DS Merchant")
requestorInfo.SetUrl("https://www.example.com/example-merchant")
merchantInfo := *basistheory.NewThreeDSMerchantInfo()
merchantInfo.SetMid("9876543210001")
merchantInfo.SetAcquirerBin("000000999")
merchantInfo.SetName("Example 3DS Merchant")
merchantInfo.SetCategoryCode("7922")
merchantInfo.SetCountryCode("826")
cardholderInfo := *basistheory.NewThreeDSCardholderInfo()
cardholderInfo.SetName("John Doe")
cardholderInfo.SetEmail("john@me.com")
authenticateThreeDSSessionRequest.SetPurchaseInfo(purchaseInfo)
authenticateThreeDSSessionRequest.SetRequestorInfo(requestorInfo)
authenticateThreeDSSessionRequest.SetMerchantInfo(merchantInfo)
authenticateThreeDSSessionRequest.SetCardholderInfo(cardholderInfo)
authenticateResponse, _, authenticateErr := apiClient.ThreeDSApi.ThreeDSAuthenticateSession(contextWithAPIKey, sessionId).AuthenticateThreeDSSessionRequest(authenticateThreeDSSessionRequest).Execute()
if authenticateErr != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": authenticateErr.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(authenticateResponse)
}
<PRIVATE_API_KEY>
with the Private API Key you created previously.And then passing that information back to your frontend.
withAuthenticationEndpoint
function in the ThreeDSServiceBuilder
for both native mobile SDK's. The authentication step is executed when the startChallenge
method is invoked.import { BasisTheory3ds } from "@basis-theory/3ds-web";
const checkout = async () => {
// initializing the 3ds sdk
const bt3ds = BasisTheory3ds("<PUBLIC_API_KEY>");
// creating the session
const session = await bt3ds.createSession({ pan: "<CARD_TOKEN_ID>" });
// authenticating the session
const authResponse = await fetch(
`{your-backend-host}/${session.id}/authenticate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
const authentication = await authResponse.json();
};
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.
Performing a Challenge
If after authenticating a 3DS session, the authentication response status
is set as challenge
, that means that the customer needs to perform a challenge, like inputting a passcode, before getting the final 3DS authentication value.
You can trigger the user challenge by calling the startChallenge
method, which returns a Promise
that will only be resolved once the user completes it.
By default, startChallenge
will time out and reject the Promise
if the user does not complete the challenge within 60000ms (1 minute). This can be configured by passing the timeout
property in the challenge object.
- Web
- iOS
- Android
- React Native
import { BasisTheory3ds } from "@basis-theory/3ds-web";
const checkout = async () => {
// initializing the 3ds sdk
const bt3ds = BasisTheory3ds("<PUBLIC_API_KEY>");
// creating the session
const session = await bt3ds.createSession({ pan: "<CARD_TOKEN_ID>" });
// authenticating the session
const authResponse = await fetch(
`{your-backend-host}/${session.id}/authenticate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
const authentication = await authResponse.json();
if (authentication.status === "challenge") {
// some information about the authentication is necessary to start a challenge
const challengeInfo = {
acsChallengeUrl: authentication.acs_challenge_url,
acsTransactionId: authentication.acs_transaction_id,
sessionId: session.id,
threeDSVersion: authentication.threeds_version,
};
// starting a challenge
// the response is just an object w/ the session id: { id: sessionId }
const challengeCompleted = await bt3ds.startChallenge(challengeInfo);
}
};
class ViewController: UIViewController {
private var threeDSService: ThreeDSService! // important to keep `threeDSService` outside of the Task, otherwise swift will handle it as a weak reference and you may experience undesired behavior
let customHeaders: [String: String] = [
"Authorization": "Bearer your_token_here",
"Trace-Id": "your_trace_id_here"
]
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
threeDSService = try ThreeDSService.builder()
.withApiKey("<YOUR PUBLIC API KEY>")
.withAuthenticationEndpoint("<YOUR AUTHENTICATION ENDPOINT>", customHeaders)
.build()
try await threeDSService.initialize { [weak self] warnings in
if let warnings = warnings, !warnings.isEmpty {
let messages = warnings.map { $0.message }.joined(separator: "\n")
print(messages)
} else {
print("No warnings.")
}
}
let session = try await threeDSService.createSession(tokenId: "<CARD TOKEN ID>")
// start challenge
try await threeDSService.startChallenge(sessionId: session.id, viewController: self,
onCompleted: { result in
print("Challenge \(result.status)")
guard let details = result.details else {
return
}
print(details)
},
onFailure: { [self] result in
print("Challenge \(result.status)")
})
} catch {
print(error.localizedDescription)
}
}
}
}
val customHeaders = Headers.Builder()
.add("Authorization", "Bearer your_token_here")
.add("Trace-Id", "your_trace_id_here")
.build();
val threeDSService = ThreeDsService.Builder()
.withApiKey("<PUBLIC_API_KEY>")
.withApplicationContext(context)
.withAuthenticationEndpoint("Your 3DS authentication endpoint URL", customHeaders) // https://developers.basistheory.com/docs/guides/process/authenticate-with-3ds#authenticating-a-session
.apply {
// make sure withSandbox is removed in production environments
if (BuildConfig.DEBUG) {
withSandbox()
}
}
.build()
val warnings = threeDSService.initialize()
if (!warnings.isNullOrEmpty()) {
// inspect warnings
}
val session = threeDSService.createSession("<CARD_TOKEN_ID>")
// start challenge
threeDsService.startChallenge(
sessionId,
activity,
::onChallengeCompleted,
::onChallengeFailed
)
fun onChallengeCompleted(result: ChallengeResponse) {
Log.i("3ds_service", "Challenge completed with status: ${result.status}");
result.details?.let {
Log.i("3ds_service", it);
}
}
fun onChallengeFailed(result: ChallengeResponse) {
Log.i("Challenge failed with status: ${result.status}")
}
import { BasisTheory3dsProvider, useBasisTheory3ds } from "@basis-theory/3ds-react-native";
const App = () => {
return (
<BasisTheory3dsProvider apiKey={"<PUBLIC_API_KEY>"}>
<MyApp />
</BasisTheory3dsProvider>
);
}
const MyApp = () => {
const { createSession, startChallenge } = useBasisTheory3ds();
//creating the session
const session = await createSession({ tokenId: "<CARD_TOKEN_ID>" });
// authenticating the session
const authResponse = await fetch(
`{your-backend-host}/${session.id}/authenticate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
const authentication = await authResponse.json();
if (authentication.status === "challenge") {
// some information about the authentication is necessary to start a challenge
const challengeInfo = {
acsChallengeUrl: authentication.acs_challenge_url,
acsTransactionId: authentication.acs_transaction_id,
sessionId: session.id,
threeDSVersion: authentication.threeds_version,
};
// starting a challenge
// the response is true if the challenge was completed, false if there was an error
const challengeCompleted = await startChallenge(challengeInfo);
}
}
Styling the Challenge Window (Web)
The internal challenge 'window' is created and handled by card issuer and unfortunately there is not much that can be done to style it to fit with the design of your website.
What is possible though, is determining how big that window can be, and style the container that holds it.
Changing the Window Size
To change the window size, use the windowSize
property on the challenge request, passing along the code for the desired pre-defined size according to the table below.
const challengeInfo = {
//... other challenge props
windowSize = '01'
};
const challengeCompleted = await basisTheory3ds.startChallenge(challengeInfo);
By default, the challenge window size is set to 03
or 500px x 600px
WindowSize ID | Size |
---|---|
01 | 250px x 400px |
02 | 390px x 400px |
03 | 500px x 600px |
04 | 600px x 400px |
05 | 100% x 100% |
Style the Window Container
The challenge window is an iframe
that gets loaded inside a predefined container div
.
The container div always has a default id - challengeFrameContainer
, and that can be used to style it:
#challengeFrameContainer {
display: none;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5)
}
Using your own Window Container
If you want more flexibility in styling, like being able to embed the window on your own content or not having to rely on absolute
positioning, you can use your own container for the challenge window.
This can be done by passing the id
for your desired container in the challengeContainerOptions
object during initialization:
import { BasisTheory3ds } from "@basis-theory/3ds-web";
const bt3ds = BasisTheory3ds("<PUBLIC_API_KEY>", {
challengeContainerOptions: { id: "your_container_id" },
});
Retrieving a Challenge Result
Once a challenge is complete, 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.
- Node
- .NET
- Python
- Go
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("<API_KEY>");
})();
app.post("/:sessionId/authenticate", async (req, res) => {
const { sessionId } = req.params;
try {
const authentication = await bt.threeds.authenticateSession(sessionId, {
authenticationCategory: "payment",
authenticationType: "payment-transaction",
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"
}
});
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);
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}`);
});
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.net.ThreeDS;
using BasisTheory.net.ThreeDS.Requests;
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 ThreeDSClient _client;
public ThreeDsApiController()
{
_client = new ThreeDSClient("<PRIVATE_API_KEY>");
}
[HttpPost("{sessionId:guid}/authenticate")]
public async Task<ActionResult> AuthenticateSession([FromRoute] Guid sessionId)
{
var authentication = await _client.AuthenticateThreeDSSessionAsync(sessionId.ToString(), new AuthenticateThreeDSSessionRequest
{
AuthenticationCategory = "payment",
AuthenticationType = "payment-transaction",
PurchaseInfo = new ThreeDSPurchaseInfo
{
Amount = "80000",
Currency = "826",
Exponent = "2",
Date = "20240109141010"
},
RequestorInfo = new ThreeDSRequestorInfo
{
Id = "example-3ds-merchant",
Name = "Example 3DS Merchant",
Url = "https://www.example.com/example-merchant"
},
MerchantInfo = new ThreeDSMerchantInfo
{
Mid = "9876543210001",
AcquirerBin = "000000999",
Name = "Example 3DS Merchant",
CategoryCode = "7922",
CountryCode = "826"
},
CardholderInfo = new ThreeDSCardholderInfo
{
Name = "John Doe",
Email = "john@me.com"
}
});
if (authentication == null)
{
return BadRequest("Failed to authenticate session.");
}
return Ok(authentication);
}
[HttpGet("{sessionId:guid}/challenge-result")]
public async Task<ActionResult> GetChallengeResult([FromRoute] Guid sessionId)
{
var result = await _client.GetChallengeResultAsync(sessionId.ToString());
if (result == null)
{
return BadRequest("Failed to get challenge result.");
}
return Ok(result);
}
}
}
In this example, we are using Basis Theory SDK and Flask Framework.
import os
from flask import Flask, request, jsonify
import basistheory
from basistheory.api import three_ds_api
from basistheory.model import AuthenticateThreeDSSessionRequest, ThreeDSPurchaseInfo, ThreeDSRequestorInfo, ThreeDSMerchantInfo, ThreeDSCardholderInfo
app = Flask(__name__)
@app.route('/<sessionId>/authenticate', methods=['POST'])
def authenticate_session(sessionId):
config = basistheory.Configuration(
host="https://api.basistheory.com",
api_key="<PRIVATE_API_KEY>"
)
with basistheory.ApiClient(configuration=config) as api_client:
three_ds_client = three_ds_api.ThreeDSApi(api_client)
authentication_request = AuthenticateThreeDSSessionRequest(
authentication_category="payment",
authentication_type="payment-transaction",
purchase_info=ThreeDSPurchaseInfo(
amount="80000",
currency="826",
exponent="2",
date="20240109141010"
),
requestor_info=ThreeDSRequestorInfo(
id="example-3ds-merchant",
name="Example 3DS Merchant",
url="https://www.example.com/example-merchant"
),
merchant_info=ThreeDSMerchantInfo(
mid="9876543210001",
acquirer_bin="000000999",
name="Example 3DS Merchant",
category_code="7922",
country_code="826"
),
cardholder_info=ThreeDSCardholderInfo(
name="John Doe",
email="john@me.com"
)
)
authentication = three_ds_client.three_ds_authenticate_session(sessionId, authenticate_three_ds_session_request=authentication_request)
return jsonify(authentication.to_dict()), 200
@app.route('/<sessionId>/challenge-result', methods=['GET'])
def get_challenge_result(sessionId):
config = basistheory.Configuration(
host="https://api.basistheory.com",
api_key="<PRIVATE_API_KEY>"
)
with basistheory.ApiClient(configuration=config) as api_client:
three_ds_client = three_ds_api.ThreeDSApi(api_client)
result = three_ds_client.three_ds_get_challenge_result(session_id=sessionId)
return jsonify(result.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"
"log"
"net/http"
"github.com/Basis-Theory/basistheory-go/v5"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/{sessionId}/authenticate", authenticateSession).Methods("POST")
router.HandleFunc("/{sessionId}/challenge-result", getChallengeResult).Methods("GET")
addr := "localhost:4242"
log.Printf("Listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, router))
}
func authenticateSession(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionId := vars["sessionId"]
configuration := basistheory.NewConfiguration()
apiClient := basistheory.NewAPIClient(configuration)
contextWithAPIKey := context.WithValue(context.Background(), basistheory.ContextAPIKeys, map[string]basistheory.APIKey{
"ApiKey": {Key: "<PRIVATE_API_KEY>"},
})
authenticateThreeDSSessionRequest := *basistheory.NewAuthenticateThreeDSSessionRequest()
authenticateThreeDSSessionRequest.SetAuthenticationCategory("payment")
authenticateThreeDSSessionRequest.SetAuthenticationType("payment-transaction")
purchaseInfo := *basistheory.NewThreeDSPurchaseInfo()
purchaseInfo.SetAmount("80000")
purchaseInfo.SetCurrency("826")
purchaseInfo.SetExponent("2")
purchaseInfo.SetDate("20240109141010")
requestorInfo := *basistheory.NewThreeDSRequestorInfo()
requestorInfo.SetId("example-3ds-merchant")
requestorInfo.SetName("Example 3DS Merchant")
requestorInfo.SetUrl("https://www.example.com/example-merchant")
merchantInfo := *basistheory.NewThreeDSMerchantInfo()
merchantInfo.SetMid("9876543210001")
merchantInfo.SetAcquirerBin("000000999")
merchantInfo.SetName("Example 3DS Merchant")
merchantInfo.SetCategoryCode("7922")
merchantInfo.SetCountryCode("826")
cardholderInfo := *basistheory.NewThreeDSCardholderInfo()
cardholderInfo.SetName("John Doe")
cardholderInfo.SetEmail("john@me.com")
authenticateThreeDSSessionRequest.SetPurchaseInfo(purchaseInfo)
authenticateThreeDSSessionRequest.SetRequestorInfo(requestorInfo)
authenticateThreeDSSessionRequest.SetMerchantInfo(merchantInfo)
authenticateThreeDSSessionRequest.SetCardholderInfo(cardholderInfo)
authenticateResponse, _, authenticateErr := apiClient.ThreeDSApi.ThreeDSAuthenticateSession(contextWithAPIKey, sessionId).AuthenticateThreeDSSessionRequest(authenticateThreeDSSessionRequest).Execute()
if authenticateErr != nil {
rw.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(rw).Encode(map[string]string{"error": authenticateErr.Error()})
return
}
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(authenticateResponse)
}
func getChallengeResult(rw http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
sessionId := vars["sessionId"]
configuration := basistheory.NewConfiguration()
apiClient := basistheory.NewAPIClient(configuration)
contextWithAPIKey := context.WithValue(context.Background(), basistheory.ContextAPIKeys, map[string]basistheory.APIKey{
"ApiKey": {Key: "<PRIVATE_API_KEY>"},
})
challengeResult, _, err := apiClient.ThreeDSApi.ThreeDSGetChallengeResult(contextWithAPIKey, sessionId).Execute()
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(challengeResult)
}
And then passing that information along back to your frontend.
- Web
- iOS
- Android
- React Native
import { BasisTheory3ds } from "@basis-theory/3ds-web";
const checkout = async () => {
// initializing the 3ds sdk
const bt3ds = BasisTheory3ds("<PUBLIC_API_KEY>");
// creating the session
const session = await bt3ds.createSession({ pan: "<CARD_TOKEN_ID>" });
// authenticating the session
const authResponse = await fetch(
`{your-backend-host}/${session.id}/authenticate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
const authentication = await authResponse.json();
if (authentication.status === "challenge") {
// some information about the authentication is necessary to start a challenge
const challengeInfo = {
acsChallengeUrl: authentication.acs_challenge_url,
acsTransactionId: authentication.acs_transaction_id,
sessionId: session.id,
threeDSVersion: authentication.threeds_version,
};
// starting a challenge
// the response is just an object w/ the session id: { id: sessionId }
const challengeCompleted = await bt3ds.startChallenge(challengeInfo);
// getting challenge result
const resultResponse = await fetch(
`{your-backend-host}/${session.id}/challenge-result`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await resultResponse.json();
}
};
let customHeaders: [String: String] = [
"Authorization": "Bearer your_token_here",
"Trace-Id": "your_trace_id_here"
]
private var threeDSService: ThreeDSService!
private var sessionId: String? = nil
threeDSService = try ThreeDSService.builder()
.withApiKey("<PUBLIC_API_KEY>")
.withAuthenticationEndpoint("Your 3DS authentication endpoint URL", customHeaders) // https://developers.basistheory.com/docs/guides/process/authenticate-with-3ds#authenticating-a-session
.withSandbox() // make sure withSandbox is removed in production environments
.build()
Task {
try await threeDSService.initialize { [weak self] warnings in
if let warnings = warnings, !warnings.isEmpty {
// inspect warnings
}
}
let session = try await self.threeDSService.createSession(tokenId: "<CARD_TOKEN_ID>")
try await self.threeDSService.startChallenge(
sessionId: session.id, viewController: self,
onCompleted: { result in
print("Challenge completed with status: \(result.status)")
guard let details = result.details else {
return
}
print(details)
},
onFailure: { result in
print("Challenge failed with status: \(result.status)")
})
// getting challenge result
let endpoint = URL(string: "{your-backend-host}/challenge-result")!
let jsonBody: [String: String] = [
"sessionId": session.id
]
let requestBody = try JSONSerialization.data(withJSONObject: jsonBody, options: [])
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = requestBody
let (data, _) = try await URLSession.shared.data(for: request)
let decodedResponse = try JSONDecoder().decode(ChallengeResult.self, from: data)
print(decodedResponse.authenticationStatus)
guard let reason = decodedResponse.authenticationStatusReason else {
return
}
print(reason)
}
val customHeaders = Headers.Builder()
.add("Authorization", "Bearer your_token_here")
.add("Trace-Id", "your_trace_id_here")
.build();
val threeDSService = ThreeDsService.Builder()
.withApiKey("<PUBLIC_API_KEY>")
.withApplicationContext(context)
.withAuthenticationEndpoint("Your 3DS authentication endpoint URL", customHeaders) // https://developers.basistheory.com/docs/guides/process/authenticate-with-3ds#authenticating-a-session
.apply {
// make sure withSandbox is removed in production environments
if (BuildConfig.DEBUG) {
withSandbox()
}
}
.build()
val warnings = threeDSService.initialize()
if (!warnings.isNullOrEmpty()) {
// inspect warnings
}
val session = threeDSService.createSession("<CARD_TOKEN_ID>")
threeDsService.startChallenge(
sessionId,
activity,
::onChallengeCompleted,
::onChallengeFailed
)
fun onChallengeCompleted(result: ChallengeResponse) {
Log.i("3ds_service", "Challenge completed with status: ${result.status}");
result.details?.let {
Log.i("3ds_service", it);
}
}
fun onChallengeFailed(result: ChallengeResponse) {
Log.i("Challenge failed with status: ${result.status}")
}
// getting challenge result
val client = OkHttpClient()
val payload = JSONObject().apply {
put("sessionId", sessionId)
}.toString()
val req = Request.Builder()
.url("{your-backend-host}/challenge-result")
.post(payload.toRequestBody("application/json".toMediaType()))
.build()
client.newCall(req)
.execute().use {
val responseBody = requireNotNull(it.body?.string())
it.body?.close()
if (!it.isSuccessful) {
throw Error("Unable to authenticate, downstream service responded ${it.code} for session $sessionId, ${it.message}")
}
Log.i(responseBody)
}
import { BasisTheory3dsProvider, useBasisTheory3ds } from "@basis-theory/3ds-react-native";
const App = () => {
return (
<BasisTheory3dsProvider apiKey={"<PUBLIC_API_KEY>"}>
<MyApp />
</BasisTheory3dsProvider>
);
}
const MyApp = () => {
const { createSession, startChallenge } = useBasisTheory3ds();
//creating the session
const session = await createSession({ tokenId: "<CARD_TOKEN_ID>" });
// authenticating the session
const authResponse = await fetch(
`{your-backend-host}/${session.id}/authenticate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
const authentication = await authResponse.json();
if (authentication.status === "challenge") {
// some information about the authentication is necessary to start a challenge
const challengeInfo = {
acsChallengeUrl: authentication.acs_challenge_url,
acsTransactionId: authentication.acs_transaction_id,
sessionId: session.id,
threeDSVersion: authentication.threeds_version,
};
// starting a challenge
// the response is true if the challenge was completed, false if there was an error
const challengeCompleted = await startChallenge(challengeInfo);
}
// getting challenge result
const resultResponse = await fetch(
`{your-backend-host}/${session.id}/challenge-result`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await resultResponse.json();
}
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.