Skip to content

3DS transactions

Integrate 3D Secure (3DS) into your checkout.

Overview

By implementing 3DS authentication into your payment flow, you benefit from:

  • Additional security: 3DS adds multiple layers of authentication and risk assessment
  • Liability shift: Successful 3DS authentication typically shifts fraud liability from merchant to card issuer
  • Higher success rate: Banks are more likely to approve 3DS-authenticated transactions

However, the 3DS payment flow is longer than the non-3DS one due to the additional authentication steps. It may also require active customer participation if a challenge is presented.

Payment flow

The 3D Secure flow is made up of nine key steps.

Step 1: Submission

The customer taps the submit button or the payment is triggered programmatically. The submit() method in CardSubmitComponent is invoked and validation occurs inside it. If validation passes, the method continues with the payment processing. If it fails, the method exits early and doesn't proceed with the transaction.

Step 2: Card tokenisation

If it's a new card, the SDK sends the card details to the tokenisation service. If it's a saved card, the SDK retrieves the existing token.

Step 3: Evaluation

The SDK evaluates whether 3DS authentication is required. In this flow, 3DS is required based on factors like transaction amount, risk assessment, or regulatory requirements.

Step 4: Pre-initiation

This is the initial step in the 3DS authentication flow. It establishes the authentication session by sending transaction and card details to the payment processor.

This step has two associated callbacks:

  • onPreInitiateAuthentication: () -> PreInitiateIntegratedAuthenticationData?

    • Purpose: Returns configuration for the authentication setup.
    • Return: PreInitiateIntegratedAuthenticationData containing acquirer profile, provider ID, timeout, and authentication indicators.
    • Return null: To skip 3DS authentication (e.g., for low-risk transactions).
  • onPostInitiateAuthentication: (AuthenticationResult) -> Unit

    • Purpose: Receives the result of the pre-initiation call.
    • Parameter: AuthenticationResult (either PreInitiateSuccessAuthenticationResult or FailedAuthenticationResult).
    • Contains: Authentication ID, state, SCA mandate status, applicable exemptions.

Step 5: Fingerprinting

During this step, device information and browser characteristics are collected by the fingerprinting component. It creates a hidden iframe that submits transaction details to the issuer's fingerprint URL, enabling risk assessment based on the user's device profile.

Step 6: Authentication

The 3DS server evaluates the transaction risk and determines the authentication path:

  • Frictionless flow: If the transaction is low-risk, authentication completes automatically without customer interaction.
  • Challenge flow: If additional verification is needed, the customer completes the 3DS authentication challenge (PIN entry, SMS code, biometric verification, etc.)

The authentication step has two associated callbacks:

  • onPreAuthentication: (PreInitiateSuccessAuthenticationResult) -> InitiateIntegratedAuthenticationData?

    • Purpose: Configures the main authentication parameters.
    • Parameter: PreInitiateSuccessAuthenticationResult containing pre-initiation results.
    • Return: InitiateIntegratedAuthenticationData with challenge window size, timeout, callback URL.
    • Return null: To abort the authentication process.
  • onPostAuthentication: (AuthenticationResult) -> Unit

    • Purpose: Receives the result of the authentication call.
    • Parameter: AuthenticationResult (either MerchantAuthenticationResult or FailedAuthenticationResult).
    • Contains: authenticationId and threeDSecureSupported. Retrieve full authentication details from your backend when successful.

Step 7: Authentication result

The SDK receives the 3DS authentication result indicating whether authentication was successful, failed, or requires additional action.

Step 8: Authorisation

This is the final step of the 3DS authentication flow. You receive the transaction data along with the 3DS authentication results and decide whether to proceed. At this point, you can still add additional data or cancel the transaction entirely. The SDK then sends the authorisation request to the payment gateway, including the 3DS authentication data.

The authorisation step has two associated callbacks:

  • onPreAuthorisation: (PreAuthorisationData) -> TransactionInitiationData?

    • Purpose: Provides final transaction data, including 3DS authentication results. This is your last chance to modify the transaction before authorisation.
    • Parameter: PreAuthorisationData containing authentication results, transaction details, and 3DS data.
    • Return: TransactionInitiationData with SCA exemption data, additional transaction parameters.
    • Return null: To proceed with default authorisation settings.
  • onPostAuthorisation: (SubmitResult) -> Unit

    • Purpose: Receives the final transaction result from the payment gateway.
    • Parameter: SubmitResult (one of AuthorisedSubmitResult, CapturedSubmitResult, RefusedSubmitResult, FailedSubmitResult).
    • Contains: Final transaction status, provider response, authentication confirmation, transaction reference.
    • Usage: Handle payment success/failure, navigate to appropriate screens, update UI state.

Step 9: Authorisation result

You receive the final authorisation response from the payment gateway. The transaction is either approved or declined and final transaction details are available, along with 3DS authentication confirmation.

Additional 3DS callbacks

Beyond the core flow callbacks, the SDK provides additional callbacks for monitoring specific 3DS events:

Authentication status callbacks

  • onAuthenticationStarted: () -> Unit

    • Purpose: Called when 3DS authentication begins.
    • Usage: Show loading indicators, update UI state.
  • onAuthenticationCompleted: () -> Unit

    • Purpose: Called when 3DS authentication ends (success or failure).
    • Usage: Hide loading indicators, reset UI state.
  • onAuthenticationSuccess: (ThreeDSAuthenticationData) -> Unit

    • Purpose: Called specifically for successful authentication.
    • Parameter: Complete ThreeDSAuthenticationData with all 3DS fields.
    • Usage: Log successful authentication, track analytics.
  • onAuthenticationFailure: (String, String?) -> Unit

    • Purpose: Called specifically for failed authentication.
    • Parameter 1: Error message describing the failure.
    • Parameter 2: Optional error code for categorising failures.
    • Usage: Handle authentication errors, display appropriate user messages.

Challenge flow callbacks

  • onChallengeStarted: () -> Unit

    • Purpose: Called when a 3DS challenge begins.
    • Usage: Prepare UI for challenge presentation.
  • onChallengeCompleted: (Boolean) -> Unit

    • Purpose: Called when a 3DS challenge ends.
    • Parameter: Boolean indicating whether challenge was successful.
    • Usage: Handle challenge results, update payment flow state.

Implementation

Before you start

To use 3D Secure in your application, you first need to enable it in the Unity Portal:

  1. In the Unity Portal, go to Merchant setup > Merchant groups.
  2. Select a merchant group.
  3. Click the Services tab.
  4. Click Edit in the Card service row.
  5. Click Configure modules in the top right.
  6. Click the toggle next to ThreeD secure service.

You'll also need to get the following from your payment processor:

  • acquirerProfileId: Your acquirer profile identifier.
  • providerId: Your 3DS provider identifier.
  • Test credentials for the sandbox environment.

Step 1: Configure your SDK

To start, set up your PxpSdkConfig to include the shopper object.

val sdkConfig = PxpSdkConfig(
    environment = Environment.TEST,
    session = SessionConfig(
        sessionId = "your_session_id",
        sessionData = "your_session_data"
    ),
    transactionData = TransactionData(
        amount = 99.99,
        currency = CurrencyType.USD,
        entryType = EntryType.ECOM,
        intent = IntentType.AUTHORISATION,
        merchantTransactionId = "order-123",
        merchantTransactionDate = { Instant.now().toString() },
        // Include 3DS-friendly data
        shopper = Shopper(
            email = "customer@example.com",
            firstName = "John",
            lastName = "Doe"
        )
    ),
    clientId = "your_client_id",
    ownerId = "Unity",
    ownerType = "MerchantGroup",
    merchantShopperId = "shopper-123"
)

Step 2: Implement callbacks

Next, implement your chosen callbacks. Note that some are required.

val cardSubmitConfig = CardSubmitComponentConfig(
    // REQUIRED: Provide 3DS configuration
    onPreInitiateAuthentication = {
        PreInitiateIntegratedAuthenticationData(
            acquirerProfileId = "your_acquirer_profile_id",
            providerId = "your_3ds_provider_id",
            requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
            timeout = 120
        )
    },

    // OPTIONAL: Handle the pre-initiation result
    onPostInitiateAuthentication = { result ->
        Log.d("3DS", "3DS pre-initiation completed: $result")
        when (result) {
            is PreInitiateSuccessAuthenticationResult -> {
                Log.d("3DS", "3DS setup successful")
            }
            is FailedAuthenticationResult -> {
                Log.e("3DS", "3DS setup failed: ${result.errorReason}")
            }
        }
    },
    
    // REQUIRED: Configure main authentication
    onPreAuthentication = { preInitResult ->
        Log.d("3DS", "Configuring 3DS authentication: $preInitResult")
        
        InitiateIntegratedAuthenticationData(
            merchantCountryNumericCode = "840",
            merchantLegalName = "Your company name",
            challengeWindowSize = "04",
            requestorChallengeIndicator = "01",
            challengeCallbackUrl = "https://your-domain.com/3ds-callback",
            timeout = 300
        )
    },

    // OPTIONAL: Handle the authentication result
    onPostAuthentication = { result ->
        when (result) {
            is FailedAuthenticationResult -> {
                Log.e("3DS", "Authentication failed: ${result.errorReason}")
            }
            is MerchantAuthenticationResult -> {
                Log.d("3DS", "Authentication ID: ${result.authenticationId}")
                Log.d("3DS", "3DS supported: ${result.threeDSecureSupported}")

                val authId = result.authenticationId ?: return@onPostAuthentication

                lifecycleScope.launch {
                    // Send authenticationId to your backend to retrieve authentication result
                    val authResult = fetchAuthenticationResultFromBackend(authId)

                    // Your backend evaluates authentication result to update authorisation decision
                    evaluateAuthenticationAndUpdateAuthorization(authResult)
                }
            }
        }
    },
    
    // REQUIRED: Final transaction approval
    onPreAuthorisation = { preAuthData ->
        Log.d("3DS", "Pre-authorisation data: $preAuthData")
        
        // Return transaction initiation data with risk screening
        TransactionInitiationData(
            psd2Data = null, // Set to Psd2Data(scaExemption = ...) if needed
            riskScreeningData = RiskScreeningData(
                performRiskScreening = true,
                excludeDeviceData = false,
                userIp = "192.168.1.100",
                account = RiskScreeningAccount(
                    id = "user_12345678",
                    creationDateTime = "2024-01-15T10:30:00.000Z"
                ),
                items = listOf(
                    RiskScreeningItem(
                        price = 89.99,
                        quantity = 1,
                        category = "Electronics",
                        sku = "CARD-PROD-001"
                    )
                ),
                fulfillments = listOf(
                    RiskScreeningFulfillment(
                        type = FulfillmentType.SHIPPED,
                        shipping = RiskScreeningShipping(
                            shippingMethod = ShippingMethod.EXPRESS
                        ),
                        recipientPerson = RiskScreeningRecipientPerson(
                            phoneNumber = "+1234567890",
                            email = "customer@example.com"
                        )
                    )
                )
            )
        )
    },

    // OPTIONAL: Handle the final result
    onPostAuthorisation = { result ->
        when (result) {
            is AuthorizedSubmitResult -> {
                Log.d("3DS", "Payment successful with 3DS!")
                Log.d("3DS", "Provider response: ${result.providerResponse.message}")
                // Navigate to success screen
                navigateToSuccessScreen()
            }
            is CapturedSubmitResult -> {
                Log.d("3DS", "Payment captured with 3DS!")
                Log.d("3DS", "Provider response: ${result.providerResponse.message}")
                navigateToSuccessScreen()
            }
            is RefusedSubmitResult -> {
                Log.e("3DS", "Payment refused: ${result.stateData.message}")
                showError("Payment was declined: ${result.stateData.message}")
            }
            is FailedSubmitResult -> {
                Log.e("3DS", "Payment failed: ${result.errorReason}")
                showError("Payment failed: ${result.errorReason}")
            }
            else -> {
                Log.e("3DS", "Unknown result type: ${result::class.simpleName}")
                showError("Payment completed with unknown status")
            }
        }
    }
)

val cardSubmitComponent = pxpCheckout.createComponent<CardSubmitComponent, CardSubmitComponentConfig>(
    ComponentType.CARD_SUBMIT,
    cardSubmitConfig
)

Step 3: Handle common scenarios

Conditional 3DS

Use the following snippet to only trigger 3DS transactions above a certain amount.

class Conditional3DSManager(private val transactionAmount: Double) {
    
    fun createCardSubmitConfig(): CardSubmitComponentConfig {
        return CardSubmitComponentConfig(
onPreInitiateAuthentication = {
                if (transactionAmount > 100.0) {
    PreInitiateIntegratedAuthenticationData(
                        acquirerProfileId = "your_profile",
                        providerId = "your_provider",
                        requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
                        timeout = 120
                    )
                } else {
                    // Return null to skip 3DS for small amounts
                    null
                }
            }
        )
    }
}

Different transaction types

Use the following snippet to handle different types of transactions.

class TransactionType3DSManager {
    
    fun get3DSConfig(transactionType: TransactionType): PreInitiateIntegratedAuthenticationData {
        val baseConfig = PreInitiateIntegratedAuthenticationData(
            acquirerProfileId = "your_profile",
            providerId = "your_provider",
            timeout = 120
        )

        return when (transactionType) {
            TransactionType.PAYMENT -> baseConfig.copy(
                requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION
            )
            TransactionType.RECURRING -> baseConfig.copy(
                requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.RECURRING_TRANSACTION
            )
            TransactionType.ADD_CARD -> baseConfig.copy(
                requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.ADD_CARD
            )
        }
    }
    
    fun createCardSubmitConfig(transactionType: TransactionType): CardSubmitComponentConfig {
        return CardSubmitComponentConfig(
            onPreInitiateAuthentication = {
                get3DSConfig(transactionType)
            }
        )
    }
}

enum class TransactionType {
    PAYMENT, RECURRING, ADD_CARD
}

Step 4: Handle errors

Lastly, make sure to implement proper error handling.

class ThreeDS3DSErrorHandler {
    
    fun createCardSubmitConfigWithErrorHandling(): CardSubmitComponentConfig {
        return CardSubmitComponentConfig(
            onSubmitError = { error ->
                Log.e("3DS", "Payment error: $error")
                handleSubmitError(error)
            },

            onPostAuthentication = { result ->
                when (result) {
                    is FailedAuthenticationResult -> {
                        Log.e("3DS", "Authentication failed: ${result.errorReason}")
                        // Don't proceed to authorisation
                        return@CardSubmitComponentConfig
                    }
                    is MerchantAuthenticationResult -> {
                        Log.d("3DS", "Authentication ID: ${result.authenticationId}")
                        Log.d("3DS", "3DS supported: ${result.threeDSecureSupported}")

                        val authId = result.authenticationId ?: return@CardSubmitComponentConfig

                        lifecycleScope.launch {
                            val authResult = fetchAuthenticationResultFromBackend(authId)

                            if (authResult.state == "AuthenticationFailed") {
                                showError("Card authentication failed")
                                return@launch
                            }

                            Log.d("3DS", "Authentication successful, proceeding to payment")
                        }
                    }
                }
            }
        )
    }
    
    private fun handleSubmitError(error: PaymentError) {
        // Handle specific 3DS errors
        when (error.code) {
            "AUTHENTICATION_FAILED" -> {
                showError("Payment authentication failed. Please try again.")
            }
            "CHALLENGE_TIMEOUT" -> {
                showError("Authentication timed out. Please try again.")
            }
            "AUTHENTICATION_REJECTED" -> {
                showError("Payment was rejected by your bank.")
            }
            "TOKEN_VAULT_EXCEPTION" -> {
                showError("Token vault exception.")
            }
            "VALIDATION_EXCEPTION" -> {
                showError("Validation failed.")
            }
            "TRANSACTION_AUTHENTICATION_REJECTED" -> {
                showError("Payment was rejected by your bank.")
            }
            "PRE_INITIATE_AUTHENTICATION_FAILED" -> {
                showError("Pre-initiate authentication failed.")
            }
            "TRANSACTION_AUTHENTICATION_REQUIRES_SCA_EXEMPTION" -> {
                showError("Transaction authentication requires SCA exemption.")
            }
            "TRANSACTION_AUTHENTICATION_INVALID" -> {
                showError("Transaction authentication is invalid.")
            }
            "NETWORK_SDK_EXCEPTION" -> {
                showError("Network error occurred. Please try again.")
            }
            "UNEXPECTED_SDK_EXCEPTION" -> {
                showError("Payment failed. Please try again.")
            }
            else -> {
                showError("Payment failed. Please try again.")
            }
        }
    }
    
    private fun showError(message: String) {
        // Implementation depends on your UI framework
        Log.e("3DS", message)
    }
}

Complete example

The following example shows a simple 3DS implementation using the new card component in a complete Android Activity.

class ThreeDSPaymentActivity : ComponentActivity() {
    private lateinit var pxpCheckout: PxpCheckout
    private lateinit var newCardComponent: NewCardComponent
    private lateinit var errorHandler: ThreeDS3DSErrorHandler
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setupPxpCheckout()
        setupComponents()
        
        setContent {
                ThreeDSPaymentScreen()
        }
    }
    
    private fun setupPxpCheckout() {
        val sdkConfig = PxpSdkConfig(
            environment = Environment.TEST,
            session = SessionConfig(
                sessionId = "session-${System.currentTimeMillis()}",
                sessionData = "your_session_data"
            ),
            transactionData = TransactionData(
                amount = 99.99,
                currency = CurrencyType.USD,
                entryType = EntryType.ECOM,
                intent = IntentType.AUTHORISATION,
                merchantTransactionId = "order-${System.currentTimeMillis()}",
                merchantTransactionDate = { Instant.now().toString() },
                shopper = Shopper(
                    email = "customer@example.com",
                    firstName = "John",
                    lastName = "Doe"
                )
            ),
            clientId = "your_client_id",
            ownerId = "Unity",
            ownerType = "MerchantGroup",
            merchantShopperId = "shopper-123"
        )
        
        pxpCheckout = PxpCheckout.builder()
            .withConfig(sdkConfig)
            .withContext(this)
            .withDebugMode(true)
            .build()
    }
    
    private fun setupComponents() {
        errorHandler = ThreeDS3DSErrorHandler()
        
        val newCardConfig = NewCardComponentConfig(
            submit = CardSubmitComponentConfig(
                // Step 1: Set up 3DS
                onPreInitiateAuthentication = {
                    PreInitiateIntegratedAuthenticationData(
                        acquirerProfileId = "your_acquirer_profile_id",
                        providerId = "your_3ds_provider_id",
                        requestorAuthenticationIndicator = RequestorAuthenticationIndicatorType.PAYMENT_TRANSACTION,
                        timeout = 120
                    )
                },

                // Step 2: Configure authentication
                onPreAuthentication = {
                    InitiateIntegratedAuthenticationData(
                        merchantCountryNumericCode = "840",
                        merchantLegalName = "Your Company Ltd",
                        challengeWindowSize = "04",
                        requestorChallengeIndicator = "01",
                        challengeCallbackUrl = "https://your-domain.com/3ds-callback",
                        timeout = 300
                    )
                },

                // Step 3: Handle final authorisation
                onPreAuthorisation = { preAuthData ->
                    Log.d("3DS", "Pre-authorisation data: $preAuthData")
                    TransactionInitiationData(
                        psd2Data = null, // Set to Psd2Data(scaExemption = ...) if needed
                        riskScreeningData = RiskScreeningData(
                            performRiskScreening = true,
                            userIp = "192.168.1.100",
                            account = RiskScreeningAccount(
                                id = "user_12345678",
                                creationDateTime = "2024-01-15T10:30:00.000Z"
                            ),
                            fulfillments = listOf(
                                RiskScreeningFulfillment(
                                    type = FulfillmentType.SHIPPED,
                                    recipientPerson = RiskScreeningRecipientPerson(
                                        phoneNumber = "+1234567890"
                                    )
                                )
                            )
                        )
                    )
                },

                // Step 4: Handle success/failure
                onPostAuthorisation = { result ->
                    when (result) {
                        is AuthorizedSubmitResult -> {
                            navigateToSuccessScreen()
                        }
                        is CapturedSubmitResult -> {
                            navigateToSuccessScreen()
                        }
                        is RefusedSubmitResult -> {
                            showError("Payment was declined: ${result.stateData.message}")
                        }
                        else -> {
                            showError("Payment failed")
                        }
                    }
                },

                // Step 5: Error handling
                onSubmitError = { error ->
                    Log.e("3DS", "3DS Error: $error")
                    showError("Payment authentication failed")
                }
            )
        )
        
        newCardComponent = pxpCheckout.createComponent(ComponentType.NEW_CARD, newCardConfig)
    }
    
    @Composable
    private fun ThreeDSPaymentScreen() {
        var isLoading by remember { mutableStateOf(false) }
        var errorMessage by remember { mutableStateOf<String?>(null) }
        var authenticationState by remember { mutableStateOf<String?>(null) }
        
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            Text(
                text = "3DS Secure Payment",
                style = MaterialTheme.typography.headlineMedium,
                modifier = Modifier.padding(bottom = 16.dp)
            )
            
            // Show authentication status
            authenticationState?.let { state ->
                Card(
                    colors = CardDefaults.cardColors(containerColor = Color.Blue.copy(alpha = 0.1f)),
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(bottom = 16.dp)
                ) {
            Text(
                        text = "Authentication Status: $state",
                        color = Color.Blue,
                        modifier = Modifier.padding(16.dp)
                    )
                }
            }
            
            // Show error message if any
            errorMessage?.let { message ->
                Card(
                    colors = CardDefaults.cardColors(containerColor = Color.Red.copy(alpha = 0.1f)),
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(bottom = 16.dp)
                ) {
                    Text(
                        text = message,
                        color = Color.Red,
                        modifier = Modifier.padding(16.dp)
                    )
                }
            }
            
            // Render payment components
            pxpCheckout.buildComponentView(
                component = newCardComponent,
                modifier = Modifier.fillMaxWidth()
            )
            
            // Loading indicator
            if (isLoading) {
                Box(
                    modifier = Modifier.fillMaxWidth(),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                    Text(
                        text = "Processing 3DS Authentication...",
                        modifier = Modifier.padding(top = 60.dp)
                    )
                }
            }
        }
    }
    
    private fun navigateToSuccessScreen(transactionId: String?) {
        val intent = Intent(this, PaymentSuccessActivity::class.java).apply {
            putExtra("transaction_id", transactionId)
            putExtra("payment_type", "3ds")
        }
        startActivity(intent)
        finish()
    }
    
    private fun showError(message: String) {
        AlertDialog.Builder(this)
            .setTitle("3DS Payment Error")
            .setMessage(message)
            .setPositiveButton("OK") { dialog, _ ->
                dialog.dismiss()
            }
            .show()
    }
}

Callback data

This section describes the data received by the different callbacks as part of the 3DS flow.

The onPreInitiateAuthentication callback doesn't receive anything so isn't included. Instead, it returns your 3DS configuration in the PreInitiateAuthenticationData object.

onPostInitiateAuthentication

The onPostInitiateAuthentication callback receives a MerchantAuthenticationResult on success, or a FailedAuthenticationResult on failure. On success, the SDK exposes two fields: authenticationId and threeDSecureSupported. Use the ID to retrieve full pre-initiate details from your backend.

When both authenticationId is absent and threeDSecureSupported is false, the SDK skips the remaining 3DS flow and doesn't invoke onPreAuthentication. This early-exit check applies when useUnityAuthenticationStrategy is false. When useUnityAuthenticationStrategy is true, the SDK uses Unity's authentication strategy API instead.

Success

When successful, onPostInitiateAuthentication receives MerchantAuthenticationResult:

MerchantAuthenticationResult(
    authenticationId = "auth_12345",
    threeDSecureSupported = true
)
ParameterDescription
authenticationIdThe unique identifier for this 3DS session. May be null when 3DS isn't required.
threeDSecureSupportedWhether 3DS authentication is supported for this transaction.

Use the authenticationId with the Get 3DS pre-initiate authentication details API to retrieve SCA mandates, exemptions, and state.

Here's an example of what to do with this data:

onPostInitiateAuthentication = { result ->
    when (result) {
        is FailedAuthenticationResult -> {
            Log.w("3DS", "3DS pre-initiation failed: ${result.errorReason}")
            return@onPostInitiateAuthentication
        }
        is MerchantAuthenticationResult -> {
            Log.d("3DS", "Authentication ID: ${result.authenticationId}")
            Log.d("3DS", "3DS supported: ${result.threeDSecureSupported}")

            if (result.authenticationId.isNullOrEmpty() && !result.threeDSecureSupported) {
                Log.d("3DS", "3DS not supported — SDK skips authentication")
                return@onPostInitiateAuthentication
            }

            val authId = result.authenticationId ?: return@onPostInitiateAuthentication

            lifecycleScope.launch {
                val authResult = fetchAuthenticationResultFromBackend(authId)

                if (authResult.scaMandated) {
                    showUserMessage("Additional verification required")
                }

                evaluateAuthenticationAndUpdateSession(authResult)
            }
        }
    }
}

Failure

When unsuccessful, onPostInitiateAuthentication receives the following FailedAuthenticationResult.

FailedAuthenticationResult(
    errorCode = "3DS_001",
    errorReason = "Invalid acquirer profile",
    correlationId = "corr_98765",
    details = listOf(
        "The specified acquirer profile is not valid",
        "Please check your configuration"
    ),
    status = 400
)
ParameterDescription
errorCodeThe error code.
errorReasonThe reason for the error.
correlationIdThe correlation ID.
detailsAdditional details about the error.
statusThe HTTP status code.

onPreAuthentication

The onPreAuthentication callback receives no parameters. Use the authentication decision evaluated after onPostInitiateAuthentication to determine whether to proceed, then return InitiateIntegratedAuthenticationData.

Here's an example of what to do with this data:

onPreAuthentication = {
    val decision = getAuthenticationDecision()
    if (!decision.proceed) {
        return@onPreAuthentication null
    }

    InitiateIntegratedAuthenticationData(
        merchantCountryNumericCode = "840",
        merchantLegalName = "Your company name",
        challengeWindowSize = "04",
        requestorChallengeIndicator = decision.challengeIndicator,
        challengeCallbackUrl = "https://your-domain.com/3ds-callback",
        timeout = 300
    )
}

onPostAuthentication

The onPostAuthentication callback receives a MerchantAuthenticationResult on success, or a FailedAuthenticationResult on failure. On success, the SDK exposes two fields: authenticationId and threeDSecureSupported — the same fields as onPostInitiateAuthentication. Use the ID to retrieve full authentication details from your backend when authentication is successful.

Success

When successful, onPostAuthentication receives MerchantAuthenticationResult:

MerchantAuthenticationResult(
    authenticationId = "auth_12345",
    threeDSecureSupported = true
)
ParameterDescription
authenticationIdThe unique identifier for the authentication attempt. Use this ID to retrieve full authentication details from the Unity backend.
threeDSecureSupportedWhether 3DS authentication is supported for this transaction.

Use the authenticationId with the Get 3DS authentication details API to retrieve the full authentication results including transaction status, ECI values, and CAVV data.

Here's an example of what to do with this data:

onPostAuthentication = { result ->
    when (result) {
        is FailedAuthenticationResult -> {
            Log.e("3DS", "Authentication failed: ${result.errorReason}")
            showError("Card verification failed. Please try a different payment method.")
            return@onPostAuthentication // Stop the payment flow
        }
        is MerchantAuthenticationResult -> {
            Log.d("3DS", "Authentication ID: ${result.authenticationId}")
            Log.d("3DS", "3DS supported: ${result.threeDSecureSupported}")

            val authId = result.authenticationId ?: return@onPostAuthentication

            lifecycleScope.launch {
                // Send authenticationId to your backend to retrieve authentication result
                val authResult = fetchAuthenticationResultFromBackend(authId)

                Log.d("3DS", "Authentication result: $authResult")

                // Check transaction status
                when (authResult.transactionStatus) {
                    "Y" -> {
                        Log.d("3DS", "Authentication successful - no challenge needed")
                        showMessage("Card verified successfully")
                    }
                    "C" -> {
                        Log.d("3DS", "Challenge completed - checking result...")
                        if (authResult.state == "AuthenticationSuccessful") {
                            Log.d("3DS", "Challenge completed successfully")
                            showMessage("Verification completed")
                        } else {
                            Log.e("3DS", "Challenge failed")
                            showError("Verification failed. Please try again.")
                            return@launch // Stop payment
                        }
                    }
                    "N" -> {
                        Log.e("3DS", "Authentication failed")
                        showError("Card verification failed")
                        return@launch // Stop payment
                    }
                    "R" -> {
                        Log.e("3DS", "Authentication rejected")
                        showError("Payment was rejected by your bank")
                        return@launch // Stop payment
                    }
                }

                // Your backend evaluates authentication result to update authorisation decision
                evaluateAuthenticationAndUpdateAuthorization(authResult)

                Log.d("3DS", "Proceeding to final authorisation...")
            }
        }
    }
}

private fun showMessage(message: String) {
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

private fun showError(message: String) {
    AlertDialog.Builder(this)
        .setTitle("Authentication Error")
        .setMessage(message)
        .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
        .show()
}

Retrieved authentication data structure

When you retrieve the full authentication result from your backend, it will include detailed 3DS data:

InitiateIntegratedSuccessAuthenticationResult(
    uniqueId = "unique_12345",
    state = "completed",
    transactionStatus = "Y", // AuthenticationVerificationSuccessful
    electronicCommerceIndicator = "05", // 3DS authenticated
    exemptionGranted = false,
    exemptionGrantedByIssuer = "79", // NoExemptionApplied
    acsUrl = null, // No challenge needed
    challengeData = null, // No challenge needed
    stateData = StateData(
        code = "success",
        reason = "Authentication completed successfully"
    ),
    cardholderInfo = null
)
InitiateIntegratedSuccessAuthenticationResult(
    uniqueId = "unique_67890",
    state = "challenge_required",
    transactionStatus = "C",
    electronicCommerceIndicator = "07",
    exemptionGranted = false,
    exemptionGrantedByIssuer = "79",
    acsUrl = "https://acs.bank.com/challenge",
    challengeData = "eyJhbGciOiJIUzI1NiJ9...",
    stateData = StateData(
        code = "pending",
        reason = "Challenge required from cardholder"
    ),
    cardholderInfo = "Additional details about the cardholder"
)
ParameterDescription
uniqueIdThe unique identifier for this 3DS session.
stateThe state of the authentication.
transactionStatusThe status of the transaction.

Possible values:
  • Y: Authentication verification successful
  • N: Not authenticated / not verified
  • U: Authentication couldn't be performed
  • A: Attempts processing performed
  • C: Challenge required
  • R: Authentication rejected
  • I: Informational only
electronicCommerceIndicatorThe Electronic Commerce Indicator (ECI).

Possible values:
  • 01: 3DS not available (non-3DS transaction)
  • 02: 3DS available but not used
  • 05: 3DS authentication successful (fully authenticated)
  • 06: 3DS authentication attempted
  • 07: 3DS authentication failed but transaction allowed
exemptionGrantedWhether an exemption was granted.
exemptionGrantedByIssuerThe type of exemption granted by the issuer.

Possible values:
  • 05: Transaction risk analysis exemption
  • 08: Trust list exemption
  • 10: Low value exemption
  • 11: Secure corporate payments exemption
  • 79: No exemption applied
acsUrlThe ACS URL.
challengeDataBase64 encoded challenge data.
stateDataDetails about the state.
cardholderInfoAdditional details about the cardholder.

onPreAuthorisation

The onPreAuthorisation callback receives:

  • Pre-authorisation data (preAuthData): Transaction data ready for authorisation.
  • Error data (error): Any error that occurred during pre-authorisation.

The callback should return the final TransactionInitiationData to be used for authorisation.

Pre-authorisation data

The pre-authorisation data includes transaction initiation data (for new cards) or card token data (for saved cards).

PreAuthorisationData(
    transactionInitiationData = TransactionInitiationData(
        psd2Data = PSD2Data(
            scaExemption = null
        ),
        threeDSecureData = ThreeDSecureData(
            threeDSecureVersion = "2.2.0",
            electronicCommerceIndicator = "05",
            cardHolderAuthenticationVerificationValue = "jGvQIvG/5UhjAREALGYYemQLXPI=",
            directoryServerTransactionId = "ds_trans_12345",
            threeDSecureTransactionStatus = "Y"
        ),
        identityVerification = IdentityVerification(
            nameVerification = true
        ),
        addressVerification = AddressVerification(
            countryCode = "US",
            houseNumberOrName = "123",
            postalCode = "10001"
        )
    )
)

Here's an example of what to do with this data:

onPreAuthorisation = { preAuthData ->
    Log.d("3DS", "Final authorisation data: $preAuthData")
    
    // Create transaction initiation data with risk screening
    // Note: TransactionInitiationData only supports psd2Data and riskScreeningData
    TransactionInitiationData(
        psd2Data = null, // Set to Psd2Data(scaExemption = ...) if needed
        riskScreeningData = RiskScreeningData(
            performRiskScreening = true,
            excludeDeviceData = false,
            userIp = "192.168.1.100",
            account = RiskScreeningAccount(
                id = "user_12345678",
                creationDateTime = "2024-01-15T10:30:00.000Z"
            ),
            items = listOf(
                RiskScreeningItem(
                    price = 89.99,
                    quantity = 1,
                    category = "Electronics"
                )
            ),
            fulfillments = listOf(
                RiskScreeningFulfillment(
                    type = FulfillmentType.SHIPPED,
                    shipping = RiskScreeningShipping(
                        shippingMethod = ShippingMethod.EXPRESS
                    ),
                    recipientPerson = RiskScreeningRecipientPerson(
                        phoneNumber = "+1234567890",
                        email = "customer@example.com"
                    )
                )
            )
        )
    )
}