Integrate a frictionless checkout experience.
By implementing non-3DS transactions into your payment flow, you benefit from:
- Streamlined checkout: No additional authentication required, providing faster payment completion
- Lower transaction friction: Reduced cart abandonment with seamless payment flow
- Faster processing: Immediate payment completion without authentication delays
- Better mobile experience: Optimised for mobile checkouts where 3DS challenges can be problematic
Non-3DS transactions are ideal for low-risk scenarios, trusted customers, or when implementing other fraud prevention measures. However, they may have different liability arrangements and potentially higher processing fees.
The non-3DS flow is made up of six key steps.
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.
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.
The SDK evaluates whether 3DS authentication is required. In this flow, no 3DS authentication is performed - the payment proceeds directly to authorisation.
The SDK sends the transaction to the payment processor for authorisation without requiring additional customer authentication.
Depending on your configuration, the transaction may be automatically captured or require manual capture later.
The payment is completed and your application receives the final result through the onPostAuthorisation callback.
Non-3DS transactions use simplified callbacks without authentication complexity:
onPostAuthorisation:(SubmitResult) -> Unit- Purpose: Receives the final transaction result from the payment gateway
- Parameter:
SubmitResult(one ofAuthorisedSubmitResult,CapturedSubmitResult,RefusedSubmitResult,FailedSubmitResult) - Contains: Transaction status, provider response, transaction reference, processing details
- Usage: Handle payment success/failure, navigate to appropriate screens, update order status
onPreAuthorisation:(PreAuthorisationData) -> TransactionInitiationData?(optional)- Purpose: Last chance to modify transaction data before authorization
- Parameter:
PreAuthorisationDatacontaining transaction details (no 3DS data) - Return:
TransactionInitiationDatawith additional metadata, exemption data, or custom parameters - Return null: To proceed with default authorization settings
onPreTokenisation:() -> Boolean(optional)- Purpose: Called before card tokenization begins
- Return:
trueto proceed with tokenization,falseto abort - Usage: Perform final validation, show loading states
onPostTokenisation:(CardTokenisationResult) -> Unit(optional)- Purpose: Receives tokenization results
- Parameter:
CardTokenisationResult(either success with token ID or failure with error) - Usage: Handle tokenization success/failure, store token references
onSubmitError:(BaseSdkException) -> Unit(optional)- Purpose: Handles submission errors
- Parameter:
BaseSdkExceptioncontaining error details and codes - Usage: Display error messages, implement retry logic, track failures
To process non-3DS transactions, configure your SDK without 3DS callbacks:
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() },
shopper = Shopper(
email = "customer@example.com",
firstName = "John",
lastName = "Doe"
)
),
clientId = "your_client_id",
ownerId = "Unity",
ownerType = "MerchantGroup",
merchantShopperId = "shopper-123"
)
val pxpCheckout = PxpCheckout.builder()
.withConfig(sdkConfig)
.withContext(this)
.withDebugMode(true)
.build()val cardNumberConfig = CardNumberComponentConfig(
label = "Card number",
isRequired = true,
validateOnChange = true
)
val cardExpiryConfig = CardExpiryDateComponentConfig(
label = "Expiry date",
isRequired = true,
validateOnChange = true
)
val cardCvcConfig = CardCvcComponentConfig(
label = "Security code",
isRequired = true,
validateOnChange = true
)
val cardSubmitConfig = CardSubmitComponentConfig(
buttonText = "Pay £99.99",
// Note: No 3DS callbacks = non-3DS transaction
onPostAuthorisation = { result ->
when (result) {
is AuthorizedSubmitResult -> {
Log.d("NonThreeDS", "Payment successful: ${result.providerResponse.message}")
navigateToSuccessScreen()
}
is CapturedSubmitResult -> {
Log.d("NonThreeDS", "Payment captured: ${result.providerResponse.message}")
navigateToSuccessScreen()
}
is RefusedSubmitResult -> {
Log.e("NonThreeDS", "Payment declined: ${result.stateData.message}")
showError("Payment was declined by your bank")
}
is FailedSubmitResult -> {
Log.e("NonThreeDS", "Payment failed: ${result.errorReason}")
showError("Payment failed. Please try again.")
}
else -> {
Log.e("NonThreeDS", "Unknown result: ${result::class.simpleName}")
showError("Payment completed with unknown status.")
}
}
},
onSubmitError = { error ->
Log.e("NonThreeDS", "Payment error: ${error.message}")
showError("Payment failed: ${error.message}")
}
)
val cardNumberComponent = pxpCheckout.createComponent(ComponentType.CARD_NUMBER, cardNumberConfig)
val cardExpiryComponent = pxpCheckout.createComponent(ComponentType.CARD_EXPIRY_DATE, cardExpiryConfig)
val cardCvcComponent = pxpCheckout.createComponent(ComponentType.CARD_CVC, cardCvcConfig)
val cardSubmitComponent = pxpCheckout.createComponent(ComponentType.CARD_SUBMIT, cardSubmitConfig)For low-value transactions where 3DS exemptions apply:
class LowValuePaymentManager {
fun createNon3DSConfig(amount: Double): CardSubmitComponentConfig {
return CardSubmitComponentConfig(
buttonText = "Pay ${formatCurrency(amount)}",
onPostAuthorisation = { result ->
when (result) {
is AuthorizedSubmitResult -> {
Log.d("LowValue", "Low-value payment successful: ${result.providerResponse.message}")
// Track successful low-value payment
trackPaymentSuccess(amount, "low_value_non_3ds")
navigateToSuccessScreen()
}
is RefusedSubmitResult -> {
Log.e("LowValue", "Low-value payment declined: ${result.stateData.message}")
showError("Payment declined. Please try a different card.")
}
else -> {
Log.e("LowValue", "Low-value payment failed")
showError("Payment failed. Please try again.")
}
}
}
)
}
private fun formatCurrency(amount: Double): String {
return "£%.2f".format(amount)
}
private fun trackPaymentSuccess(amount: Double, type: String) {
// Analytics tracking for successful payments
Log.d("Analytics", "Payment successful: amount=$amount, type=$type")
}
}For returning customers with established trust:
class TrustedCustomerPaymentManager(private val customerId: String) {
fun createTrustedCustomerConfig(): CardSubmitComponentConfig {
return CardSubmitComponentConfig(
buttonText = "Pay securely",
onPreAuthorisation = { preAuthData ->
// Add trusted customer metadata
TransactionInitiationData(
psd2Data = preAuthData.psd2Data,
metadata = mapOf(
"customer_id" to customerId,
"trusted_customer" to "true",
"risk_level" to "low",
"transaction_type" to "non_3ds_trusted"
)
)
},
onPostAuthorisation = { result ->
when (result) {
is AuthorizedSubmitResult -> {
Log.d("Trusted", "Trusted customer payment successful")
updateCustomerTrustScore(customerId, "successful_payment")
navigateToSuccessScreen()
}
is RefusedSubmitResult -> {
Log.e("Trusted", "Trusted customer payment declined")
updateCustomerTrustScore(customerId, "declined_payment")
showError("Payment declined. Please verify your card details.")
}
else -> {
Log.e("Trusted", "Unknown payment result")
showError("Payment completed with unknown status.")
}
}
}
)
}
private fun updateCustomerTrustScore(customerId: String, event: String) {
Log.d("TrustScore", "Updating trust score for $customerId: $event")
}
}Implement comprehensive error handling for non-3DS transactions:
class Non3DSErrorHandler {
fun handlePaymentError(error: PaymentError) {
when (error.code) {
"CARD_DECLINED" -> {
showError("Your card was declined. Please try a different payment method.")
}
"INSUFFICIENT_FUNDS" -> {
showError("Insufficient funds. Please try a different card.")
}
"INVALID_CARD" -> {
showError("Invalid card details. Please check and try again.")
}
"EXPIRED_CARD" -> {
showError("Your card has expired. Please use a different card.")
}
"BLOCKED_CARD" -> {
showError("Your card is blocked. Please contact your bank.")
}
"NETWORK_ERROR" -> {
showError("Network error. Please check your connection and try again.")
}
"VALIDATION_FAILED" -> {
showError("Please check all required fields are completed correctly.")
}
else -> {
showError("Payment failed. Please try again or use a different payment method.")
}
}
}
private fun showError(message: String) {
// Implementation depends on your UI framework
Log.e("Non3DS", message)
}
}The following example shows a complete non-3DS implementation:
class Non3DSPaymentActivity : ComponentActivity() {
private lateinit var pxpCheckout: PxpCheckout
private lateinit var newCardComponent: NewCardComponent
private val errorHandler = Non3DSErrorHandler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupPxpCheckout()
setupComponents()
setContent {
Non3DSPaymentScreen()
}
}
private fun setupPxpCheckout() {
val sdkConfig = PxpSdkConfig(
environment = Environment.TEST,
session = SessionConfig(
sessionId = "session-${System.currentTimeMillis()}",
sessionData = "your_session_data"
),
transactionData = TransactionData(
amount = 29.99,
currency = CurrencyType.GBP,
entryType = EntryType.ECOM,
intent = IntentType.AUTHORISATION,
merchantTransactionId = "order-${System.currentTimeMillis()}",
merchantTransactionDate = { Instant.now().toString() },
shopper = Shopper(
email = "customer@example.com",
firstName = "Jane",
lastName = "Smith"
)
),
clientId = "your_client_id",
ownerId = "Unity",
ownerType = "MerchantGroup",
merchantShopperId = "shopper-456"
)
pxpCheckout = PxpCheckout.builder()
.withConfig(sdkConfig)
.withContext(this)
.withDebugMode(true)
.build()
}
private fun setupComponents() {
val newCardConfig = NewCardComponentConfig(
submit = CardSubmitComponentConfig(
// No 3DS callbacks - transaction will be non-3DS
onPostAuthorisation = { result ->
when (result) {
is AuthorizedSubmitResult -> {
Log.d("Non3DS", "Payment successful: ${result.providerResponse.message}")
navigateToSuccessScreen()
}
is CapturedSubmitResult -> {
Log.d("Non3DS", "Payment captured: ${result.providerResponse.message}")
navigateToSuccessScreen()
}
is RefusedSubmitResult -> {
Log.e("Non3DS", "Payment declined: ${result.stateData.message}")
showDeclinedMessage(result.stateData.code)
}
is FailedSubmitResult -> {
Log.e("Non3DS", "Payment failed: ${result.errorReason}")
showError("Payment failed. Please try again.")
}
else -> {
Log.e("Non3DS", "Unknown result: ${result::class.simpleName}")
showError("Payment completed with unknown status.")
}
}
},
onSubmitError = { error ->
Log.e("Non3DS", "Submit error: ${error.message}")
errorHandler.handlePaymentError(error)
}
)
)
newCardComponent = pxpCheckout.createComponent(ComponentType.NEW_CARD, newCardConfig)
}
@Composable
private fun Non3DSPaymentScreen() {
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Express checkout",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = "Fast and secure payment without additional verification",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 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 payment...",
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", "non_3ds")
}
startActivity(intent)
finish()
}
private fun showDeclinedMessage(reasonCode: String?) {
val message = when (reasonCode) {
"insufficient_funds" -> "Insufficient funds. Please try a different card."
"expired_card" -> "Your card has expired. Please use a different card."
"invalid_card" -> "Invalid card details. Please check and try again."
else -> "Payment was declined. Please try a different card."
}
showError(message)
}
private fun showError(message: String) {
AlertDialog.Builder(this)
.setTitle("Payment Error")
.setMessage(message)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
.show()
}
}This section describes the data received by the different callbacks as part of the non-3DS flow.
The onPreAuthorisation callback receives:
- Pre-authorisation data (
preAuthData): Transaction data ready for authorisation. - Error data (
error): Any error that occurred during pre-authorisation.
For non-3DS transactions, the 3DS-related data will be null.
The pre-authorisation data includes transaction initiation data (for new cards) or card token data (for saved cards).
With transaction initiation data:
PreAuthorisationData(
transactionInitiationData = TransactionInitiationData(
threeDSecureData = null, // Always null for non-3DS
psd2Data = PSD2Data(
scaExemption = "LowValue"
),
identityVerification = IdentityVerification(
nameVerification = true
),
addressVerification = AddressVerification(
countryCode = "US",
houseNumberOrName = "123",
postalCode = "10001"
)
)
)With card token data:
PreAuthorisationData(
transactionInitiationData = null,
cardTokenData = CardTokenData(
gatewayTokenId = "gw_token_abc123def456789",
schemeTokenId = null,
maskedPrimaryAccountNumber = "****-****-****-4242",
cardExpiryMonth = "12",
cardExpiryYear = "2025",
scheme = "VISA",
fundingSource = "CREDIT",
ownerType = "PERSONAL",
issuerName = "Chase Bank",
issuerCountryCode = "US",
lastSuccessfulPurchaseDate = "2024-01-15T10:30:00Z",
lastSuccessfulPayoutDate = null
)
)| Parameter | Description |
|---|---|
transactionInitiationData | Details about the transaction, if associated with a new card, null otherwise. |
transactionInitiationData.threeDSecureData | This is always null for non-3DS transactions. |
transactionInitiationData.psd2Data | Details about PSD2. This is required for non-3DS transactions. |
transactionInitiationData.psd2Data.scaExemption | The type of SCA exemption that applies to this transaction. Possible values:
|
transactionInitiationData.identityVerification | Details about the identity verification. |
transactionInitiationData.identityVerification.nameVerification | Whether the cardholder's name matches the name associated with the registered address on file. |
transactionInitiationData.addressVerification | Details about the address verification. |
transactionInitiationData.addressVerification.countryCode | The country code associated with the cardholder's address, in ISO-3166-1 alpha-2 format. |
transactionInitiationData.addressVerification.houseNumberOrName | The cardholder's street address. |
transactionInitiationData.addressVerification.postalCode | The postal or ZIP code associated with the cardholder's address. |
cardTokenData | Details about the card token if associated with a saved card, null otherwise. |
Here's an example of what to do with this data:
onPreAuthorisation = { preAuthData, error ->
Log.d("Payment", "Card transaction data: $preAuthData")
if (error != null) {
Log.e("Payment", "Pre-authorisation error: ${error.message}")
showError("Payment processing error: ${error.message}")
return@onPreAuthorisation null // Cancel the transaction
}
val transactionData = preAuthData.transactionInitiationData
// Add fraud prevention data
val enhancedData = transactionData?.copy(
// Add device fingerprinting
deviceData = DeviceData(
userAgent = getUserAgent(),
language = Locale.getDefault().language,
screenResolution = "${getScreenWidth()}x${getScreenHeight()}",
timezone = TimeZone.getDefault().id
),
// Add session information
sessionData = SessionData(
sessionId = generateSessionId(),
timestamp = Instant.now().toString(),
ipAddress = getClientIP() // You'd implement this
),
// Add custom risk indicators
riskIndicators = RiskIndicators(
customerType = "returning",
paymentHistory = "good",
velocityCheck = "passed"
)
)
Log.d("Payment", "Sending enhanced card transaction")
enhancedData
}
// Helper data classes
data class DeviceData(
val userAgent: String,
val language: String,
val screenResolution: String,
val timezone: String
)
data class SessionData(
val sessionId: String,
val timestamp: String,
val ipAddress: String?
)
data class RiskIndicators(
val customerType: String,
val paymentHistory: String,
val velocityCheck: String
)The onPostAuthorisation callback receives the final transaction result (SubmitResult).
If the transaction was successful, you'll receive either an AuthorizedSubmitResult or a CapturedSubmitResult.
AuthorizedSubmitResult(
state = "Authorised",
providerResponse = ProviderResponse(
code = "00",
message = "Approved",
cardVerificationCodeResult = "M",
addressVerificationServiceResult = "Y"
),
fundingData = FundingData(
cardVerificationCodeResult = "Matched",
addressVerificationServiceResult = "Y"
)
)| Parameter | Description |
|---|---|
state | The final state of the transaction. Possible values:
|
providerResponse | Details about the provider's response. |
providerResponse.code | The raw result code returned by the provider that processed the transaction. |
providerResponse.message | The raw message associated with the result code from the provider that processed the transaction. |
providerResponse.cardVerificationCodeResult | The Card Verification Code (CVC) result returned by the provider. |
providerResponse.addressVerificationServiceResult | The Address Verification Service (AVS) result returned by the provider. |
fundingData | Details about the payment method. |
fundingData.cardVerificationCodeResult | The Card Verification Code (CVC) result in human-readable format. |
fundingData.addressVerificationServiceResult | The Address Verification Service (AVS) result in human-readable format. |
Here's an example of what to do with this data:
onPostAuthorisation = { result ->
Log.d("Payment", "Non-3DS payment result: $result")
when (result) {
is AuthorizedSubmitResult -> {
Log.d("Payment", "Payment successful!")
Log.d("Payment", "Provider response: ${result.providerResponse.message}")
// Check verification results
val fundingData = result.fundingData
if (fundingData.cardVerificationCodeResult == "Matched") {
Log.d("Payment", "CVC verification passed")
}
if (fundingData.addressVerificationServiceResult == "Y") {
Log.d("Payment", "Address verification passed")
}
// Store transaction details
storeTransactionRecord(TransactionRecord(
amount = 99.99,
currency = "USD",
cardType = "VISA",
processingType = "non-3ds",
timestamp = Instant.now().toString()
))
// Navigate to success screen
navigateToSuccessScreen()
}
is CapturedSubmitResult -> {
Log.d("Payment", "Payment captured successfully!")
Log.d("Payment", "Provider response: ${result.providerResponse.message}")
navigateToSuccessScreen()
}
is RefusedSubmitResult -> {
Log.e("Payment", "Payment declined: ${result.stateData.message}")
handlePaymentFailure(result)
}
is FailedSubmitResult -> {
Log.e("Payment", "Payment failed: ${result.errorReason}")
showError("Payment failed: ${result.errorReason}")
}
else -> {
Log.e("Payment", "Unknown result type: ${result::class.simpleName}")
showError("Payment completed with unknown status")
}
}
}
data class TransactionRecord(
val amount: Double,
val currency: String,
val cardType: String,
val processingType: String,
val timestamp: String
)If the bank or issuer declines the transaction, you'll receive a RefusedSubmitResult.
RefusedSubmitResult(
state = "Refused",
stateData = StateData(
code = "05",
message = "Do not honour"
),
providerResponse = ProviderResponse(
code = "05",
message = "Do not honour",
merchantAdvice = MerchantAdvice(
code = "01",
message = "Try another payment method"
),
cardVerificationCodeResult = "M",
addressVerificationServiceResult = "Y"
),
fundingData = FundingData(
cardVerificationCodeResult = "Matched",
addressVerificationServiceResult = "Y"
)
)Here's an example of how to handle failures:
private fun handlePaymentFailure(result: RefusedSubmitResult) {
Log.e("Payment", "Payment declined: ${result.stateData.message}")
// Check merchant advice for next steps
result.providerResponse?.merchantAdvice?.let { advice ->
when (advice.code) {
"01" -> {
// Try another payment method
showError("Payment declined. Please try a different card.")
enableAlternativePaymentMethods()
}
"02" -> {
// Retry with different amount
showError("Transaction amount issue. Please contact support.")
}
"03" -> {
// Contact issuer
showError("Please contact your bank to authorise this payment.")
}
else -> {
showError("Payment declined: ${advice.message}")
}
}
} ?: run {
// Generic decline message
val declineReason = when (result.stateData.code) {
"05" -> "Payment declined by your bank"
"14" -> "Invalid card number"
"54" -> "Card has expired"
"61" -> "Amount limit exceeded"
else -> "Payment was declined"
}
showError(declineReason)
}
// Track decline for analytics
trackDeclineEvent(DeclineEvent(
declineCode = result.stateData.code,
declineReason = result.stateData.message,
merchantAdvice = result.providerResponse?.merchantAdvice?.code,
timestamp = Instant.now().toString()
))
}
data class DeclineEvent(
val declineCode: String,
val declineReason: String,
val merchantAdvice: String?,
val timestamp: String
)
private fun enableAlternativePaymentMethods() {
// Show alternative payment options
Log.d("Payment", "Enabling alternative payment methods")
}
private fun trackDeclineEvent(event: DeclineEvent) {
// Track decline for fraud prevention and analytics
Log.d("Analytics", "Decline event: $event")
}For both callbacks, implement comprehensive error handling to ensure a smooth user experience:
onSubmitError = { error ->
Log.e("Payment", "Submit error: ${error.message}")
when (error.errorCode) {
"NETWORK_ERROR" -> {
showError("Network connection issue. Please check your internet and try again.")
}
"VALIDATION_FAILED" -> {
showError("Please check all required fields are completed correctly.")
}
"CARD_EXPIRED" -> {
showError("Your card has expired. Please use a different card.")
}
"INSUFFICIENT_FUNDS" -> {
showError("Insufficient funds. Please try a different card.")
}
else -> {
showError("Payment failed. Please try again or use a different payment method.")
}
}
}