Accept card payments with 3D Secure authentication, automatic validation, and one-click payments for returning customers.
Card payments are automatically included in the drop-in when enabled in your session. The drop-in handles all card field rendering, validation, 3D Secure authentication, and payment processing automatically.
- Card fields appear automatically in the drop-in when cards are enabled in your session configuration.
- The drop-in handles all card setup, so you don't need card-specific code.
- Card payments use the same
onSuccessandonErrorcallbacks as other payment methods for a unified integration. - Automatic field validation for card number, expiry date, and CVC.
- 3D Secure authentication is handled automatically with native Android UI.
- Returning customers can use one-click payments when their card is saved.
- PCI DSS Level 1 compliant, with no card data stored on your server.
When a customer pays with a card:
- The customer taps "Card" in the payment options list. The card form appears.
- The customer enters their card number, expiry date, and CVC.
- The drop-in validates the card details in real time.
- The customer taps "Pay" and 3D Secure authentication begins (if required).
- The customer completes authentication in the native Android UI.
- The payment is processed through Unity.
- Your
onSuccesscallback fires.
Card-specific settings are configured through methodConfig.global in the Drop-in configuration. The DropInCardConfig class is currently an empty marker and doesn't have its own properties.
The following properties in DropInGlobalConfig apply to card payments:
| Property | Description |
|---|---|
acceptedCardNetworksList<CardNetworks>? | Which card brands to accept through card payments. Falls back to session configuration if not specified. Possible values:
|
allowedCardFundingSourceList<CardFundingSource>? | Which funding types to accept (Credit, Debit, Prepaid). Possible values:
|
allowedIssuerCountryCodesList<String>? | Allowed card issuer countries as ISO country codes (e.g., listOf("GB", "US", "FR")). |
onGetConsent(paymentMethod: PaymentMethod) -> Boolean | Controls whether to show consent checkbox for card payments. Use this to enable card-on-file functionality. |
This example shows a full card configuration using global settings:
methodConfig = DropInMethodConfig(
// Global settings that apply to card payments
global = DropInGlobalConfig(
// Restrict to specific card networks
acceptedCardNetworks = listOf(
CardNetworks.VISA,
CardNetworks.MASTERCARD,
CardNetworks.AMERICAN_EXPRESS
),
// Allow only credit and debit cards
allowedCardFundingSource = listOf(
CardFundingSource.CREDIT,
CardFundingSource.DEBIT
),
// Restrict to cards issued in specific countries
allowedIssuerCountryCodes = listOf("GB", "US", "FR"),
// Control consent checkbox for cards
onGetConsent = { paymentMethod ->
// Show consent checkbox for cards to save payment method
paymentMethod == PaymentMethod.CARD
}
),
// Card config is currently an empty marker
card = DropInCardConfig()
)Card payments require the following to function correctly:
- Android compatibility: Android 8.0 (API level 26) or higher.
- HTTPS: Your backend endpoints must be served over HTTPS.
- Unity Portal configuration: Cards must be enabled and configured in the Unity Portal.
- Entry type: Cards support
EcomandMotoentry types. - 3D Secure: Your merchant account must be configured for 3D Secure authentication.
One-click payments require the shopper to have previously authorised your merchant account. Authorisations must be captured within the time window specified by your payment scheme (typically 7-30 days).
Card payments work through the standard implementation, with no card-specific code needed:
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
import com.pxp.checkout.models.DropInTransactionData
import com.pxp.checkout.models.DropInTransactionIntentData
import com.pxp.checkout.models.Environment
import com.pxp.checkout.models.EntryType
import com.pxp.checkout.models.IntentType
import com.pxp.checkout.checkoutdropin.types.DropInSubmitResult
import com.pxp.checkout.exceptions.BaseSdkException
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@Composable
fun CheckoutScreen() {
val context = LocalContext.current
var sessionData by remember { mutableStateOf<SessionData?>(null) }
var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
var isLoading by remember { mutableStateOf(true) }
// Fetch session from backend
LaunchedEffect(Unit) {
try {
val response = apiClient.post("/api/create-session") {
contentType(ContentType.Application.Json)
}
if (response.status.value == 200) {
val result = response.body<SessionResponse>()
if (result.success && result.data != null) {
sessionData = result.data
} else {
Log.e("Checkout", "Failed to create session: ${result.error}")
}
}
} catch (e: Exception) {
Log.e("Checkout", "Error creating session", e)
} finally {
isLoading = false
}
}
// Initialize Drop-in once session is loaded
LaunchedEffect(sessionData) {
sessionData?.let { session ->
val checkoutDropIn = CheckoutDropIn.initialize(
context = context,
config = CheckoutDropInConfig(
environment = Environment.TEST,
session = session,
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = DropInTransactionData(
currency = "GBP",
amount = 99.99,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Authorisation
),
merchant = "Demo Store",
merchantTransactionId = { UUID.randomUUID().toString() },
merchantTransactionDate = { Instant.now().toString() }
),
onGetShopper = {
// Provide shopper ID for vaulting
Shopper(id = "shopper-123")
},
onSuccess = { result: DropInSubmitResult ->
Log.d("Checkout", "Card payment successful!")
Log.d("Checkout", "System transaction ID: ${result.systemTransactionId}")
Log.d("Checkout", "Payment method: ${result.paymentMethod}")
// CRITICAL: Verify on backend
verifyPaymentOnBackend(result)
},
onError = { error: BaseSdkException ->
Log.e("Checkout", "Card payment failed", error)
Toast.makeText(
context,
"Payment failed: ${error.message}",
Toast.LENGTH_LONG
).show()
}
)
)
checkoutDropInComponent = checkoutDropIn.create()
}
}
// Render Drop-in
if (isLoading) {
CircularProgressIndicator()
} else {
checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
}
}Enable cards in your session request:
// BACKEND: Create a session with cards enabled
const sessionRequest = {
merchant: "MERCHANT-1",
site: "SITE-1",
sessionTimeout: 120,
merchantTransactionId: crypto.randomUUID(),
transactionMethod: {
intent: {
card: "Authorisation" // or "Purchase"
}
},
amounts: {
currencyCode: "GBP",
transactionValue: 99.99
},
allowedFundingTypes: {
cards: {
// Card configuration from the Unity Portal will be used
// You can optionally specify allowed card brands here
allowedCardBrands: ["visa", "mastercard", "amex"]
}
},
allowTransaction: true,
serviceType: "CheckoutDropIn"
};Drop-in supports two card payment flows, configured via the intent parameter:
When a card payment succeeds, your onSuccess callback receives the same standard result as other payment methods:
onSuccess = { result: DropInSubmitResult ->
Log.d("Checkout", "Payment details:")
Log.d("Checkout", "- System transaction ID: ${result.systemTransactionId}")
Log.d("Checkout", "- Merchant transaction ID: ${result.merchantTransactionId}")
Log.d("Checkout", "- Payment method: ${result.paymentMethod}") // "Card"
// Note: Amount, currency, card details must be retrieved from backend
// 3D Secure authentication data is handled internally
}Handle card-specific errors:
import com.pxp.checkout.exceptions.BaseSdkException
onError = { error: BaseSdkException ->
Log.e("Checkout", "Error code: ${error.code}")
Log.e("Checkout", "Error message: ${error.message}")
// Handle specific error types
when {
error.message?.contains("declined", ignoreCase = true) == true -> {
Toast.makeText(
context,
"Card declined. Please try a different card.",
Toast.LENGTH_LONG
).show()
}
error.message?.contains("insufficient", ignoreCase = true) == true -> {
Toast.makeText(
context,
"Insufficient funds. Please use a different card.",
Toast.LENGTH_LONG
).show()
}
error.message?.contains("expired", ignoreCase = true) == true -> {
Toast.makeText(
context,
"Card expired. Please use a different card.",
Toast.LENGTH_LONG
).show()
}
error.message?.contains("invalid", ignoreCase = true) == true -> {
Toast.makeText(
context,
"Invalid card details. Please check and try again.",
Toast.LENGTH_LONG
).show()
}
error.message?.contains("3DS", ignoreCase = true) == true ||
error.message?.contains("authentication", ignoreCase = true) == true -> {
Toast.makeText(
context,
"Authentication failed. Please try again.",
Toast.LENGTH_LONG
).show()
}
else -> {
Toast.makeText(
context,
"Payment failed: ${error.message}",
Toast.LENGTH_LONG
).show()
}
}
}The following table describes common card error scenarios:
| Scenario | How to detect | Recommended action |
|---|---|---|
| Card declined | error.message contains "declined" | Suggest trying different card |
| Insufficient funds | error.message contains "insufficient" | Suggest using different card or payment method |
| Card expired | error.message contains "expired" | Request valid card details |
| Invalid card | error.message contains "invalid" | Ask customer to check card details |
| 3D Secure failed | error.message contains "3DS" or "authentication" | Suggest trying again or contacting bank |
| Network error | error.message contains "network" or "timeout" | Retry payment after brief delay |
Card errors return descriptive messages rather than specific error code constants. Check the error.message property for error details. For production apps, implement robust error handling with retry logic and user-friendly messaging.
Always verify card payments on your backend to ensure payment success before fulfilling orders:
import com.pxp.checkout.checkoutdropin.types.DropInSubmitResult
onSuccess = { result: DropInSubmitResult ->
// Send to backend for verification
coroutineScope.launch {
try {
val response = apiClient.post("/api/verify-payment") {
contentType(ContentType.Application.Json)
setBody(mapOf(
"systemTransactionId" to result.systemTransactionId,
"merchantTransactionId" to result.merchantTransactionId
))
}
if (response.status.value == 200) {
val verified = response.body<VerificationResponse>()
if (verified.success) {
// Navigate to success screen
navController.navigate("success?orderId=${verified.orderId}")
} else {
Toast.makeText(
context,
"Payment verification failed",
Toast.LENGTH_LONG
).show()
}
}
} catch (e: Exception) {
Log.e("Checkout", "Verification error", e)
Toast.makeText(
context,
"Failed to verify payment",
Toast.LENGTH_LONG
).show()
}
}
}Use the following backend code to verify card transactions via the PXP API:
// BACKEND: Verify card payment
app.post('/api/verify-payment', async (req, res) => {
const { systemTransactionId, merchantTransactionId } = req.body;
try {
// Query the PXP API to get transaction details
const txnPath = `api/v1/transactions/${systemTransactionId}`;
const { authHeader, requestId } = createAuthHeader(
txnPath,
'',
process.env.PXP_TOKEN_ID,
process.env.PXP_TOKEN_VALUE
);
const transaction = await fetch(
`https://api-services.pxp.io/${txnPath}`,
{
headers: {
'X-Client-Id': process.env.PXP_CLIENT_ID,
'X-Request-Id': requestId,
'Authorization': authHeader
}
}
).then(r => r.json());
// Verify transaction state
if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
return res.json({ success: false, error: 'Transaction not successful' });
}
// Verify merchant transaction ID matches
if (transaction.merchantTransactionId !== merchantTransactionId) {
return res.json({ success: false, error: 'Transaction ID mismatch' });
}
// Verify amount matches expected amount from your order records
const order = await getOrderByMerchantTransactionId(merchantTransactionId);
const txnAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
if (Math.abs(txnAmount - order.amount) > 0.01) {
return res.json({ success: false, error: 'Amount mismatch' });
}
// Verify funding type is card
const fundingType = transaction.fundingData?.fundingType ||
transaction.fundingType ||
'Unknown';
if (fundingType !== 'Card') {
return res.json({ success: false, error: 'Invalid funding type' });
}
// Fulfill order
const orderId = await fulfillOrder(transaction);
return res.json({ success: true, orderId });
} catch (error) {
console.error('Verification error:', error);
return res.json({ success: false, error: 'Verification failed' });
}
});Card vaulting allows returning customers to pay with one click by saving their card. When enabled, customers who have previously saved a card can skip entering card details entirely.
- The customer pays with a card and agrees to save it.
- Unity vaults the card and returns a vault ID.
- On return visit,
onGetShopperprovides the shopper ID. - Drop-in enables one-click card payment (no fields).
- The customer taps "Pay with saved card" and payment completes instantly.
Implement onGetShopper to enable vaulting:
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
import com.pxp.checkout.models.DropInTransactionData
import com.pxp.checkout.models.DropInTransactionIntentData
import com.pxp.checkout.models.Environment
import com.pxp.checkout.models.EntryType
import com.pxp.checkout.models.IntentType
import com.pxp.checkout.checkoutdropin.types.DropInSubmitResult
import com.pxp.checkout.exceptions.BaseSdkException
import com.pxp.checkout.services.models.transaction.Shopper
@Composable
fun CheckoutScreen() {
val context = LocalContext.current
var sessionData by remember { mutableStateOf<SessionData?>(null) }
var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
// Fetch session from backend
LaunchedEffect(Unit) {
sessionData = fetchSessionFromBackend()
}
// Initialize Drop-in once session is loaded
LaunchedEffect(sessionData) {
sessionData?.let { session ->
val checkoutDropIn = CheckoutDropIn.initialize(
context = context,
config = CheckoutDropInConfig(
environment = Environment.TEST,
session = session,
ownerType = "MerchantGroup",
ownerId = "MERCHANT-1",
transactionData = DropInTransactionData(
currency = "GBP",
amount = 99.99,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Purchase
),
merchant = "Demo Store",
merchantTransactionId = { UUID.randomUUID().toString() },
merchantTransactionDate = { Instant.now().toString() }
),
// REQUIRED: Provide shopper ID to enable card vaulting
onGetShopper = {
val user = getCurrentUser()
Shopper(id = user.shopperId) // e.g., Shopper(id = "shopper-123")
},
onSuccess = { result: DropInSubmitResult ->
verifyPaymentOnBackend(result)
navController.navigate("success")
},
onError = { error: BaseSdkException ->
Log.e("Checkout", "Card payment failed", error)
Toast.makeText(
context,
"Payment failed: ${error.message}",
Toast.LENGTH_LONG
).show()
}
)
)
checkoutDropInComponent = checkoutDropIn.create()
}
}
// Render Drop-in
checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
}When onGetShopper returns a shopper ID, the SDK automatically:
- Fetches saved cards from the PXP API.
- Enables one-click payment for vaulted cards.
- Handles vault setup during the first payment.
Card vaulting is enabled by implementing onGetShopper and optionally controlling consent with methodConfig.global.onGetConsent:
CheckoutDropInConfig(
// ... other config
onGetShopper = {
val user = getCurrentUser()
Shopper(id = user.shopperId) // Required for vaulting
},
methodConfig = DropInMethodConfig(
global = DropInGlobalConfig(
onGetConsent = { paymentMethod ->
// Control consent checkbox for card vaulting
paymentMethod == PaymentMethod.CARD
}
),
card = DropInCardConfig() // Empty marker
)
)3D Secure authentication is handled automatically by the drop-in. When a card requires 3D Secure, the drop-in:
- Detects that 3D Secure is required from the payment response.
- Launches the native Android 3D Secure UI.
- Guides the customer through authentication (OTP, biometric, etc.).
- Processes the authenticated payment.
- Fires your
onSuccesscallback on completion.
No additional code is required for 3D Secure authentication. The drop-in manages the entire flow automatically.
3D Secure UI is rendered using native Android components and adapts to the authentication method required by the card issuer (OTP, biometric, challenge questions, etc.). The UI is fully PCI DSS Level 1 compliant.
For subscriptions and recurring charges, use the IntentType.Authorisation flow with card vaulting enabled:
import com.pxp.checkout.models.DropInTransactionData
import com.pxp.checkout.models.DropInTransactionIntentData
import com.pxp.checkout.models.EntryType
import com.pxp.checkout.models.IntentType
transactionData = DropInTransactionData(
currency = "GBP",
amount = 9.99,
entryType = EntryType.Ecom,
intent = DropInTransactionIntentData(
card = IntentType.Authorisation // Use Authorisation for recurring
),
merchant = "Demo Store",
merchantTransactionId = { UUID.randomUUID().toString() },
merchantTransactionDate = { Instant.now().toString() }
)After the first payment, use the vaulted card token to process subsequent recurring payments via the PXP API without customer interaction.