Learn how to implement and manage recurring payments with PXP Android Components.
Recurring payments allow merchants to automatically charge customers on a regular basis for subscriptions, memberships, or ongoing services. The PXP Android SDK supports recurring payment implementation through card tokenisation and proper transaction configuration.
By implementing recurring payments, you can:
- Provide a seamless subscription experience for your customers
- Reduce customer churn through automated billing
- Ensure compliance with regional payment regulations
- Improve cash flow predictability for your business
The Android SDK implements recurring payments through a card tokenisation approach:
- Initial payment setup: The customer provides their card details and consents to recurring billing.
- Card tokenisation: Card details are securely tokenised and stored.
- Subsequent payments: Use stored tokens for future transactions without requiring customer interaction.
Configure recurring payment information using the RecurringData class in your transaction configuration:
data class RecurringData(
val frequencyInDays: Int? = null,
val frequencyExpiration: String? = null
)Configure your SDK with recurring transaction data to establish the subscription:
val transactionData = TransactionData(
amount = 9.99,
currency = CurrencyType.USD,
entryType = EntryType.Ecom,
intent = IntentType.Authorisation,
merchantTransactionId = "subscription-setup-${UUID.randomUUID()}",
merchantTransactionDate = { Instant.now().toString() },
recurring = RecurringData(
frequencyInDays = 30, // Monthly billing
frequencyExpiration = LocalDateTime.now().plusYears(1).toString()
),
shopper = Shopper(
email = "customer@example.com",
firstName = "John",
lastName = "Doe"
)
)
val sdkConfig = PxpSdkConfig(
environment = Environment.TEST,
session = sessionConfig,
transactionData = transactionData,
clientId = "your-client-id",
merchantShopperId = "customer-123",
ownerType = "MerchantGroup",
ownerId = "UnityGroup"
)Use the card-on-file component to manage stored payment methods for recurring billing:
val cardOnFileConfig = CardOnFileComponentConfig().apply {
// Configure for recurring payment selection
onTokenSelected = { token ->
Log.d("Recurring", "Selected token for recurring payment: ${token.id}")
// Store token reference for future billing cycles
storeRecurringPaymentToken(token)
}
onTokensLoaded = { tokens ->
Log.d("Recurring", "Available payment methods: ${tokens.size}")
// Filter tokens suitable for recurring payments
val recurringTokens = tokens.filter { it.isRecurringEnabled }
displayRecurringPaymentOptions(recurringTokens)
}
}When customers add a new payment method for recurring billing:
val newCardConfig = NewCardComponentConfig().apply {
// Configure card consent for recurring payments
fields.cardConsent = CardConsentConfig(
label = "I authorise recurring charges to this payment method",
isRequired = true,
onToggleChanged = { isConsented ->
if (isConsented) {
Log.d("Recurring", "Customer consented to recurring billing")
enableRecurringPaymentSetup()
}
}
)
}Configure the card submit component to handle recurring payment processing:
val cardSubmitConfig = CardSubmitComponentConfig().apply {
// Set component references
newCardComponent = newCardComponent
// Configure recurring payment callbacks
onPreTokenisation = {
Log.d("Recurring", "Starting tokenisation for recurring payment")
true // Proceed with tokenisation
}
onPostTokenisation = { result ->
when (result) {
is CardTokenisationResult.Success -> {
Log.d("Recurring", "Token created for recurring payment: ${result.gatewayTokenId}")
// Store token for future recurring charges
saveRecurringPaymentToken(result.gatewayTokenId)
}
is CardTokenisationResult.Failure -> {
Log.e("Recurring", "Tokenisation failed: ${result.errorReason}")
handleRecurringSetupFailure(result.errorReason)
}
}
}
onPostAuthorisation = { result ->
when (result) {
is SubmitResult.Success -> {
Log.d("Recurring", "Recurring payment setup successful")
confirmRecurringPaymentSetup(result)
}
is SubmitResult.Failure -> {
Log.e("Recurring", "Recurring payment setup failed")
handleRecurringSetupFailure(result.errorMessage)
}
}
}
}Configure recurring payment frequency using frequencyInDays:
// Different billing cycles
val dailyBilling = RecurringData(frequencyInDays = 1)
val weeklyBilling = RecurringData(frequencyInDays = 7)
val monthlyBilling = RecurringData(frequencyInDays = 30)
val quarterlyBilling = RecurringData(frequencyInDays = 90)
val yearlyBilling = RecurringData(frequencyInDays = 365)
// Custom frequencies
val biWeeklyBilling = RecurringData(frequencyInDays = 14)
val semiMonthlyBilling = RecurringData(frequencyInDays = 15)Set expiration dates for recurring billing agreements:
val recurringData = RecurringData(
frequencyInDays = 30,
frequencyExpiration = LocalDateTime.now()
.plusYears(2) // Subscription expires in 2 years
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
)The first payment in a recurring series typically requires 3DS authentication:
val initialRecurringConfig = CardSubmitComponentConfig().apply {
// Enable 3DS for initial setup
onPreInitiateAuthentication = {
Log.d("Recurring", "Starting 3DS for recurring payment setup")
PreInitiateIntegratedAuthenticationData(
threeDSRequestorAuthenticationIndicator = "02" // Recurring transaction
)
}
onPostAuthentication = { result, authData ->
Log.d("Recurring", "3DS completed for recurring setup")
// Process successful authentication for recurring setup
processRecurringAuthentication(result, authData)
}
}Merchant-initiated transactions for recurring payments typically bypass 3DS:
// Configure for MIT (no 3DS required)
val mitTransactionData = TransactionData(
amount = 9.99,
currency = CurrencyType.USD,
entryType = EntryType.Ecom,
intent = IntentType.Authorisation,
merchantTransactionId = "recurring-payment-${UUID.randomUUID()}",
merchantTransactionDate = { Instant.now().toString() },
recurring = RecurringData(
frequencyInDays = 30,
frequencyExpiration = existingSubscription.expirationDate
)
)Handle various error conditions in recurring payment flows:
fun handleRecurringPaymentError(error: BaseSdkException) {
when (error.errorCode) {
"CARD_EXPIRED" -> {
Log.w("Recurring", "Card expired, request updated payment method")
requestPaymentMethodUpdate()
}
"INSUFFICIENT_FUNDS" -> {
Log.w("Recurring", "Payment failed due to insufficient funds")
scheduleRetryAttempt()
}
"CARD_DECLINED" -> {
Log.w("Recurring", "Card declined, notify customer")
notifyCustomerOfDeclinedPayment()
}
else -> {
Log.e("Recurring", "Unexpected error in recurring payment: ${error.message}")
handleGeneralRecurringError(error)
}
}
}Implement retry logic for failed recurring payments:
class RecurringPaymentManager {
private val maxRetryAttempts = 3
private var retryCount = 0
fun processRecurringPayment(tokenId: String, amount: Double) {
if (retryCount >= maxRetryAttempts) {
handleMaxRetriesExceeded()
return
}
// Configure transaction for retry
val retryTransactionData = TransactionData(
amount = amount,
currency = CurrencyType.USD,
merchantTransactionId = "retry-${retryCount}-${UUID.randomUUID()}",
merchantTransactionDate = { Instant.now().toString() },
recurring = RecurringData(
frequencyInDays = 30,
frequencyExpiration = getSubscriptionExpiration()
)
)
// Process with stored token
processPaymentWithToken(tokenId, retryTransactionData)
}
private fun handlePaymentFailure(error: BaseSdkException) {
retryCount++
if (shouldRetry(error.errorCode)) {
scheduleRetry()
} else {
handlePermanentFailure(error)
}
}
}