Skip to content

3DS Authentication — Android ​

The Cake 3DS SDK for Android (vn.cake.threedsdk) implements the EMV 3-D Secure 2.x protocol for cardholder authentication. The SDK collects and encrypts device data. It exchanges CReq and CRes challenge messages. It renders the challenge UI. It handles protocol errors. The EMVCo specification requires the SDK to communicate directly with the Directory Server (DS) and the Access Control Server (ACS).

SDK Details

Maven coordinatesvn.cake.threedsdk:cake-3ds-android:{version}
Namespacevn.cake.threedsdk
ProtocolEMV 3DS 2.1.0, 2.2.0, 2.3.0, and 2.3.1
Min SDK21
Target SDK35
LanguageKotlin (Java 11), Kotlin Coroutines for asynchronous calls

Installation ​

The SDK supports two installation methods: a local Maven repository, or a direct AAR file.

Step 1. Add the Maven repository from Cake to the root build.gradle file of your app.

groovy
allprojects {
    repositories {
        maven { url '/path/to/cake-3ds-maven-repo' }
    }
}

Step 2. Add the dependency to the build.gradle file of your app module.

groovy
dependencies {
    implementation 'vn.cake.threedsdk:cake-3ds-android:{version}'
}

Option 2: Direct AAR File ​

Step 1. Copy the AAR file from Cake to the libs/ directory of your app.

Step 2. Add the AAR file and its transitive dependencies to the build.gradle file of your app module.

groovy
dependencies {
    implementation files('libs/3ds2sdk-release.aar')

    // Transitive dependencies — add these too
    implementation 'com.nimbusds:nimbus-jose-jwt:10.4.2'
    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation 'com.google.android.material:material:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
    implementation 'androidx.core:core-ktx:1.13.1'
    implementation 'androidx.activity:activity-ktx:1.9.3'
    implementation 'androidx.fragment:fragment-ktx:1.8.6'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1'
}

Initializing the SDK ​

Create one CakeThreeDs2ServiceImpl instance for each app session. Create it in Application.onCreate(), or in a dependency injection module. The SDK offers two public constructors.

The default constructor uses a built-in SDK reference number:

kotlin
import vn.cake.threedsdk.service.CakeThreeDs2ServiceImpl
import kotlinx.coroutines.Dispatchers

val service = CakeThreeDs2ServiceImpl(
    context = applicationContext,
    enableLogging = BuildConfig.DEBUG,
    workContext = Dispatchers.IO
)

Use the second constructor when Cake gives you a specific sdkReferenceNumber:

kotlin
val service = CakeThreeDs2ServiceImpl(
    context = applicationContext,
    sdkReferenceNumber = sdkReferenceNumber,
    enableLogging = BuildConfig.DEBUG,
    workContext = Dispatchers.IO
)
ParamTypeRequiredDescription
contextContextYesApplication context
sdkReferenceNumberStringNoSDK reference identifier. Omit it to use the built-in default of the SDK.
enableLoggingBooleanNo (default false)Enables Logcat output for debugging
workContextCoroutineContextYesDispatcher used for background SDK work (device data collection, encryption)
MemberTypeDescription
sdkVersionStringCurrent SDK version
warningsList<Warning>Warnings detected during initialization, for example a rooted device or an attached debugger

When the app session ends, call service.cleanup() once to free SDK resources.

Creating a Transaction ​

For each payment that requires 3DS authentication, create a Transaction:

kotlin
val transaction = service.createTransaction(
    sdkTransactionId = SdkTransactionId.create(),
    directoryServerID = directoryServerId,       // e.g. "A000000003" for Visa
    messageVersion = null,                       // null = SDK picks the highest supported version
    isLiveMode = !BuildConfig.DEBUG,
    directoryServerName = "visa",
    rootCerts = directoryServerRootCerts,
    dsPublicKey = directoryServerPublicKey,
    keyId = null,
    uiCustomization = CakeUiCustomization.createWithAppTheme(activity)
)
ParamTypeDescription
sdkTransactionIdSdkTransactionIdUnique ID for this transaction. Call SdkTransactionId.create() to generate a UUID.
directoryServerIDStringDirectory Server ID of the card network (see the table below)
messageVersionString?Protocol version to use. Pass null to use the latest version that the SDK supports.
isLiveModeBooleanfalse uses test Directory Server certificates. true uses production certificates.
directoryServerNameStringReadable name of the DS. The SDK uses it to find the DS logo shown on the processing screen.
rootCertsList<X509Certificate>Root certificates of the DS-CA, used to check the ACS-signed content
dsPublicKeyPublicKeyPublic key of the DS, used to encrypt device data. Get it with service.getPublicKey(directoryServerID).
keyIdString?Optional key identifier when the DS publishes multiple keys
uiCustomizationCakeUiCustomizationVisual customization for the challenge screen (see Customizing the Challenge UI)

Directory Servers ​

service.getPublicKey(directoryServerID) resolves the following networks:

NetworkIDAlgorithm
VisaA000000003RSA
MastercardA000000004RSA
Test (RSA)F055545342RSA
Test (EC)F155545342EC

For a network outside this list, supply your own dsPublicKey and rootCerts to createTransaction(...) instead of service.getPublicKey(...).


Getting Authentication Request Parameters ​

Before you send the Authentication Request (AReq) to your 3DS Server, call createAuthenticationRequestParameters() for each transaction:

kotlin
lifecycleScope.launch {
    val params = transaction.createAuthenticationRequestParameters()
    // Send `params` to your backend to build and send the AReq
}

This is a suspend function. Call it from a coroutine. It collects and encrypts device data. It also generates the ephemeral key pair for the ACS secure channel.

FieldDescription
deviceDataDevice information, JWE-encrypted with the DS public key
sdkTransactionIdThe SdkTransactionId that you passed to createTransaction()
sdkAppIdUUID that identifies this app installation, generated by the SDK
sdkReferenceNumberSDK reference identifier
sdkEphemeralPublicKeyEphemeral EC P-256 public key of the SDK, given as a JWK string
messageVersionProtocol version selected for this transaction

Your backend includes these fields in the AReq. The response from the 3DS Server, the ARes, tells you the outcome. A frictionless outcome needs no further action from the SDK. Otherwise, the ARes requires the challenge flow.


Launching the Challenge ​

Register ChallengeContract before the activity or fragment reaches the STARTED state, for example as a field set in onCreate. Android requires this early registration for every ActivityResultContract.

kotlin
import vn.cake.threedsdk.transaction.ChallengeContract
import vn.cake.threedsdk.transaction.ChallengeResult

val challengeLauncher = registerForActivityResult(ChallengeContract()) { result: ChallengeResult ->
    handleChallengeResult(result)
}

fun handleChallengeResult(result: ChallengeResult) {
    when (result) {
        is ChallengeResult.Succeeded -> onAuthenticationSucceeded(result)
        is ChallengeResult.Failed -> onAuthenticationFailed(result)
        is ChallengeResult.Canceled -> onAuthenticationCanceled(result)
        is ChallengeResult.Timeout -> onAuthenticationTimedOut(result)
        is ChallengeResult.ProtocolError -> onProtocolError(result.data)
        is ChallengeResult.RuntimeError -> onRuntimeError(result.throwable)
    }
}

When the ARes requires a challenge, build ChallengeParameters from its contents. Then send the parameters to an InitChallengeRepository. The repository runs the initial CReq and CRes exchange before the SDK shows any UI. On success, pass the result to the challengeLauncher that you registered above:

kotlin
import vn.cake.threedsdk.transaction.ChallengeParameters
import vn.cake.threedsdk.transaction.IntentData
import vn.cake.threedsdk.transaction.InitChallengeRepositoryFactory
import vn.cake.threedsdk.transaction.InitChallengeResult

val challengeParameters = ChallengeParameters(
    threeDsServerTransactionId = ares.threeDsServerTransId,
    acsTransactionId = ares.acsTransId,
    acsRefNumber = ares.acsReferenceNumber,
    acsSignedContent = ares.acsSignedContent,
    dsTransactionId = ares.dsTransId
)

val initArgs = transaction.createInitChallengeArgs(
    challengeParameters = challengeParameters,
    timeoutMins = 5,
    intentData = IntentData(
        clientSecret = clientSecret,
        sourceId = sourceId,
        publishableKey = publishableKey
    )
)

val repository = InitChallengeRepositoryFactory(
    application = application,
    isLiveMode = !BuildConfig.DEBUG,
    sdkTransactionId = transaction.sdkTransactionId,
    uiCustomization = uiCustomization,
    rootCerts = directoryServerRootCerts,
    enableLogging = BuildConfig.DEBUG,
    workContext = Dispatchers.IO
).create()

lifecycleScope.launch {
    when (val result = repository.startChallenge(initArgs)) {
        is InitChallengeResult.Start ->
            // Initial CReq and CRes succeeded, launch the challenge UI
            challengeLauncher.launch(result.challengeViewArgs)
        is InitChallengeResult.End ->
            // No UI needed, for example the initial request already failed or timed out
            handleChallengeResult(result.challengeResult)
    }
}
ParamTypeDescription
challengeParametersChallengeParametersACS details from the ARes. The 3DS Server transaction ID, the ACS transaction ID, the ACS reference number, and the ACS-signed content are all mandatory.
timeoutMinsIntOverall challenge timeout, minimum 5 minutes
intentDataIntentDataOpaque data (client secret, source ID, publishable key) that the SDK returns to you unchanged in the ChallengeResult

repository.startChallenge(initArgs) validates the ACS-signed content against the DS-CA root certificates. It completes the Diffie-Hellman key exchange (ECDH P-256). The SDK uses this exchange to encrypt and decrypt the CReq and CRes messages that follow. repository.startChallenge(initArgs) also sends the first CReq. The result is a sealed InitChallengeResult:

VariantFieldsMeaning
StartchallengeViewArgs: ChallengeViewArgsThe first ACS round trip succeeded. Pass this value to challengeLauncher.launch(...) to show the challenge UI.
EndchallengeResult: ChallengeResultThe transaction ended before the SDK showed any UI, for example with ProtocolError, Timeout, or RuntimeError on the initial request. Handle it the same way as a ChallengeContract result.

Processing Screen

Call transaction.getProgressView(activity) to get a non-cancelable dialog with the DS logo and a progress indicator. Show this dialog after you send the AReq. Hide it when the challenge UI appears. The EMVCo specification requires this Processing Screen.


ChallengeResult ​

The ActivityResultContract callback delivers this sealed class:

VariantFieldsMeaning
SucceededuiTypeCode, transStatusCardholder completed the challenge successfully
FaileduiTypeCodeCardholder failed the challenge
CanceleduiTypeCode?Cardholder canceled, or abandoned the challenge
TimeoutuiTypeCode?Challenge exceeded timeoutMins
ProtocolErrordata: ErrorDataCRes failed protocol validation (see Error Handling)
RuntimeErrorthrowable: ThrowableUnexpected exception, for example a network or parsing error

Every variant carries intentData, the IntentData value that you passed in. Every variant also carries initialUiType, the UiType of the first challenge screen shown. Use these fields to match the result to your payment flow. You do not need to keep separate state.


The Challenge Screen ​

3DS challenge screen rendered by the SDK on Android

ChallengeActivity renders this screen. The CakeUiCustomization that you pass to createTransaction(...) controls its colors, fonts, and button styles. See Customizing the Challenge UI for the full set of options.


Frictionless vs. Challenge Flow ​

stateDiagram-v2
    [*] --> Created: createTransaction()
    Created --> AuthParams: createAuthenticationRequestParameters()
    AuthParams --> Frictionless: ARes, no challenge required
    AuthParams --> Challenge: ARes, challenge required
    Challenge --> Completed: ChallengeResult Succeeded or Failed
    Challenge --> Error: ChallengeResult ProtocolError or RuntimeError
    Challenge --> TimedOut: ChallengeResult Timeout
    Frictionless --> [*]
    Completed --> [*]
    Error --> [*]
    TimedOut --> [*]

Challenge UI Types ​

The ACS decides which UI the challenge screen shows. The SDK exposes this value as initialUiType, and also inside ErrorData:

UiTypeCodeRequires Submit ButtonDescription
Text01YesFree-text entry, for example an OTP
SingleSelect02YesSingle choice from a list
MultiSelect03YesMultiple choices from a list
OutOfBand04NoResolved outside the app, for example in the banking app of the cardholder
Html05NoACS-provided HTML rendered in a WebView
HtmlOob06NoHTML challenge combined with an out-of-band continue action
Information07NoInformational screen with a continue button, no data entry

Error Handling ​

ChallengeResult.ProtocolError carries an ErrorData object. This object describes why the CRes failed validation:

FieldDescription
errorCodeProtocol error code (see table below)
errorComponentWhich component reported the error: ThreeDsSdk, ThreeDsServer, DirectoryServer, or Acs
errorDescriptionHuman-readable description
errorDetailSpecific field name that failed validation, where applicable

Protocol Error Codes ​

CodeNameTrigger
101InvalidMessageReceivedThe messageType field in the CRes was not "CRes"
102UnsupportedMessageVersionThe version is missing, empty, or does not match the CReq
201RequiredDataElementMissingA required field is absent for the current UiType
202UnrecognizedCriticalMessageExtensionsAn unsupported critical message extension was present
203InvalidDataElementFormatThe format, length, or value of a field is invalid
207ReservedValueRangeA value fell in a reserved range
301InvalidTransactionIdThe transaction IDs in the CRes do not match the CReq
302DataDecryptionFailureJWE decryption of the CRes failed
402TransactionTimedoutThe transaction exceeded the configured timeout

The SDK notifies the ACS of the error automatically. This is a fire-and-forget action. The SDK returns ChallengeResult.ProtocolError to your app after this. You do not need to take any other action.


Customizing the Challenge UI ​

You can create a CakeUiCustomization in two ways.

Option 1: From Your App Theme ​

CakeUiCustomization.createWithAppTheme(activity) reads the primary color, the accent color, and the text colors from the theme of your app. It applies these colors to the toolbar, the labels, and the default buttons.

kotlin
val uiCustomization = CakeUiCustomization.createWithAppTheme(activity)

Use this option when the challenge screen must match the rest of your app automatically. You can still override individual parts afterward, as the example below shows.

Option 2: Manual Customization ​

The plain CakeUiCustomization() constructor starts with no defaults. You set every color and style yourself.

kotlin
val uiCustomization = CakeUiCustomization()

Use this option for full control over the challenge screen.

Customizing Individual Parts ​

CakeUiCustomization holds four customization objects: one for the toolbar, one for labels, one for text boxes, and one set of buttons, one per ButtonType. Each object controls a different part of the challenge screen. The example below starts from createWithAppTheme(activity) (Option 1) and overrides specific parts:

kotlin
import vn.cake.threedsdk.init.ui.CakeUiCustomization
import vn.cake.threedsdk.init.ui.CakeToolbarCustomization
import vn.cake.threedsdk.init.ui.CakeLabelCustomization
import vn.cake.threedsdk.init.ui.CakeTextBoxCustomization
import vn.cake.threedsdk.init.ui.CakeButtonCustomization
import vn.cake.threedsdk.init.ui.UiCustomization.ButtonType

val uiCustomization = CakeUiCustomization.createWithAppTheme(activity).apply {
    setAccentColor("#FF6B00")

    val toolbar = CakeToolbarCustomization().apply {
        setBackgroundColor("#1A1A1A")
        setStatusBarColor("#000000")
        setHeaderText("Secure Checkout")
        setButtonText("CANCEL")
        setTextColor("#FFFFFF")
    }
    setToolbarCustomization(toolbar)

    val labels = CakeLabelCustomization().apply {
        setHeadingTextColor("#000000")
        setHeadingTextFontSize(20)
        setTextColor("#333333")
        setTextFontSize(16)
    }
    setLabelCustomization(labels)

    val textBox = CakeTextBoxCustomization().apply {
        setBorderColor("#89000000")
        setBorderWidth(2)
        setCornerRadius(8)
        setHintTextColor("#89000000")
    }
    setTextBoxCustomization(textBox)

    val submitButton = CakeButtonCustomization().apply {
        setBackgroundColor("#FF6B00")
        setTextColor("#FFFFFF")
        setCornerRadius(8)
    }
    setButtonCustomization(submitButton, ButtonType.SUBMIT)
    setButtonCustomization(submitButton, ButtonType.NEXT)
    setButtonCustomization(submitButton, ButtonType.CONTINUE)

    val cancelButton = CakeButtonCustomization().apply {
        setBackgroundColor("#EC4847")
        setTextColor("#FFFFFF")
    }
    setButtonCustomization(cancelButton, ButtonType.CANCEL)
}

Import Note

ButtonType is nested inside the UiCustomization interface. Import it with import vn.cake.threedsdk.init.ui.UiCustomization.ButtonType.

CakeToolbarCustomization ​

MethodSets
setBackgroundColor(hex)Background color of the toolbar
setStatusBarColor(hex)Color of the status bar above the toolbar
setHeaderText(text)Title text shown in the toolbar
setButtonText(text)Text of the cancel button in the toolbar
setTextColor(hex)Color of the header text and the button text

CakeLabelCustomization ​

MethodSets
setHeadingTextColor(hex)Color of heading text
setHeadingTextFontName(name)Font of heading text
setHeadingTextFontSize(size)Font size of heading text
setTextColor(hex)Color of body label text
setTextFontName(name)Font of body label text
setTextFontSize(size)Font size of body label text

CakeTextBoxCustomization ​

MethodSets
setBorderColor(hex)Border color of the input field
setBorderWidth(dp)Border width of the input field
setCornerRadius(dp)Corner radius of the input field
setHintTextColor(hex)Color of the hint text
setTextColor(hex)Color of the text that the cardholder enters
setTextFontName(name)Font of the text that the cardholder enters
setTextFontSize(size)Font size of the text that the cardholder enters

CakeButtonCustomization ​

MethodSets
setBackgroundColor(hex)Background color of the button
setCornerRadius(dp)Corner radius of the button
setTextColor(hex)Color of the button text
setTextFontName(name)Font of the button text
setTextFontSize(size)Font size of the button text

Set one CakeButtonCustomization for each button that you want to customize. ButtonType defines seven values: SUBMIT, CONTINUE, NEXT, CANCEL, RESEND, SELECT, and ADD_CHOICE. For a button type outside this list, call setButtonCustomization(customization, "your_button_type") with a string instead.

Pass the resulting uiCustomization into service.createTransaction(...).


Full Flow Diagram ​

sequenceDiagram
    participant App as Merchant App
    participant SDK as 3DS SDK
    participant ACS as Issuer ACS

    App->>SDK: CakeThreeDs2ServiceImpl(context, enableLogging, workContext)
    SDK-->>App: CakeThreeDs2Service

    App->>SDK: createTransaction(directoryServerID, dsPublicKey, ...)
    SDK-->>App: Transaction

    App->>SDK: createAuthenticationRequestParameters()
    SDK-->>App: AuthenticationRequestParameters

    Note over App: Send AReq to your 3DS Server, receive ARes

    alt Frictionless
        Note over App: ARes indicates no challenge is needed
    else Challenge required
        App->>SDK: createInitChallengeArgs(challengeParameters, timeoutMins, intentData)
        SDK-->>App: InitChallengeArgs
        App->>SDK: startChallenge(initArgs)
        SDK->>ACS: Validate ACS-signed content, key exchange, initial CReq
        ACS-->>SDK: Initial CRes
        SDK-->>App: InitChallengeResult

        alt Start
            App->>SDK: launch(challengeViewArgs)
            SDK->>ACS: Remaining CReq and CRes exchange
            ACS-->>SDK: Challenge result
            SDK-->>App: ChallengeResult
        else End
            Note over App: Transaction ended before any UI, for example ProtocolError, Timeout, or RuntimeError
        end
    end

Developers