Skip to content

Recurring payments

Learn how to implement and manage recurring payments with PXP Android Components.

Overview

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

How recurring payments work

Card tokenisation approach

The Android SDK implements recurring payments through a card tokenisation approach:

  1. Initial payment setup: The customer provides their card details and consents to recurring billing.
  2. Card tokenisation: Card details are securely tokenised and stored.
  3. Subsequent payments: Use stored tokens for future transactions without requiring customer interaction.

Recurring transaction data

Configure recurring payment information using the RecurringData class in your transaction configuration:

data class RecurringData(
    val frequencyInDays: Int? = null,
    val frequencyExpiration: String? = null
)

Setting up recurring payments

Initial subscription setup

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"
)

Card-on-file for recurring payments

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)
    }
}

New card setup for recurring

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()
            }
        }
    )
}

Managing recurring payments

Card submit for recurring transactions

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)
            }
        }
    }
}

Frequency configuration

Common billing frequencies

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)

Expiration handling

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)
)

3DS and recurring payments

Initial setup with 3DS

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)
    }
}

Subsequent payments (MIT)

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
    )
)

Error handling

Common recurring payment scenarios

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)
        }
    }
}

Retry logic

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)
        }
    }
}