Skip to content

Recurring payments

Learn how to implement recurring payment tokens with Google Pay for subscriptions and recurring billing.

Overview

Consent payment tokens enable secure recurring payments with Google Pay. By obtaining customer consent during the initial payment, you can store a reusable payment token for future transactions without requiring the customer to re-authenticate each time.

Consent payment tokens are essential for:

  • Monthly or annual billing, such as subscription services
  • Regular scheduled charges
  • Automatic account top-ups
  • Instalment plans
  • Pay-as-you-go services
  • Membership renewals

Consent payment tokens provide a frictionless payment experience for returning customers whilst maintaining security through tokenisation and customer consent requirements.

PXP doesn't provide an automatic payment scheduler. You must implement your own scheduling system to initiate subsequent recurring charges using the PXP Transactions API.

The consent token flow involves two main phases:

  1. The customer makes their first payment via Google Pay.
  2. During payment, the customer grants consent for future charges.
  3. The SDK creates and authorises the initial payment.
  4. The backend stores encrypted payment token for future use.
  5. The customer receives confirmation of payment and consent.

Phase 2: Subsequent payments

  1. Your backend initiates recurring payments using the stored gatewayTokenId via the PXP Transactions API.
  2. The payment processes without customer interaction.
  3. The customer receives notification of the charge.
  4. The token remains valid for future transactions until expiration.

To implement recurring payments, you need to:

  1. Configure the SDK with recurring data in transactionData.
  2. Use either the google-pay-consent component or the onGetConsent callback.
  3. Provide a shopper ID via onGetShopper for consent tracking.

Recurring configuration

When initialising the SDK, include the recurring configuration in transactionData:

import com.pxp.PxpCheckout
import com.pxp.checkout.models.*
import com.pxp.checkout.services.models.transaction.Shopper
import java.text.SimpleDateFormat
import java.util.*

val pxpCheckout = PxpCheckout.builder()
    .withConfig(
        PxpSdkConfig(
            environment = Environment.TEST,
            session = sessionData,
            ownerId = "your-owner-id",
            ownerType = "MerchantGroup",
            transactionData = TransactionData(
                currency = "GBP",
                amount = 9.99,
                merchantTransactionId = UUID.randomUUID().toString(),
                merchantTransactionDate = { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date()) },
                entryType = EntryType.Ecom,
                intent = TransactionIntentData(
                    card = IntentType.Authorisation
                ),
                merchant = "Merchant Name",
                // Configure recurring payment schedule
                recurring = RecurringData(
                    frequencyInDays = 30,  // How often the customer will be charged
                    frequencyExpiration = "2027-12-31T00:00:00Z"  // When the recurring token expires
                )
            ),
            // Required: Provide customer ID for consent tracking
            onGetShopper = {
                Shopper(id = "customer-123")
            }
        )
    )
    .withContext(context)
    .build()

When you include the recurring object in your transaction data, the SDK automatically sets the processingModel to MerchantInitiatedInitialRecurring when creating the transaction request. You don't need to set this manually.

Property Description
frequencyInDays
Int
The billing frequency in days (e.g., 30 for monthly, 365 for annual).
frequencyExpiration
String?
When the recurring token should expire (ISO 8601 date string, e.g., "2027-12-31T00:00:00Z").

Configure the Google Pay button to request consent:

The SDK automatically configures the tokenizationSpecification with the correct gateway and merchant ID from your session. You only need to provide allowedPaymentMethods with the card parameters.

import com.pxp.checkout.components.googlepay.*
import com.pxp.checkout.components.googlepay.types.*

val googlePayConfig = GooglePayButtonComponentConfig().apply {
    paymentDataRequest = PaymentDataRequest(
        allowedPaymentMethods = listOf(
            PaymentMethodSpecification(
                parameters = PaymentMethodParameters(
                    allowedCardNetworks = listOf(
                        CardNetwork.VISA,
                        CardNetwork.MASTERCARD
                    ),
                    allowedAuthMethods = listOf(
                        CardAuthMethod.PAN_ONLY,
                        CardAuthMethod.CRYPTOGRAM_3DS
                    )
                )
            )
        ),
        transactionInfo = TransactionInfo(
            currencyCode = "GBP",
            totalPriceStatus = TotalPriceStatus.FINAL,
            totalPrice = "9.99"
        )
    )
    
    // Option 1: Use the consent component (recommended)
    googlePayConsentComponent = consentComponent
    
    // Option 2: Use a simple boolean consent callback
    onGetConsent = {
        true // Return true if customer has given consent
    }
}

For recurring payments, use the google-pay-consent component to collect customer consent:

import com.pxp.checkout.components.googlepayconsent.*
import com.pxp.checkout.types.ComponentType

// Create consent component
val consentComponent = pxpCheckout.createComponent<
    GooglePayConsentComponent,
    GooglePayConsentConfig
>(
    type = ComponentType.GOOGLE_PAY_CONSENT,
    config = GooglePayConsentConfig(
        label = "I agree to save my payment method for future purchases",
        initialChecked = false
    )
)

// Link consent component to Google Pay button
val googlePayConfig = GooglePayButtonComponentConfig().apply {
    googlePayConsentComponent = consentComponent
    // ... rest of configuration
}

The google-pay-consent component provides customisation options:

Property Description
label
String
The consent text displayed to the customer.
initialChecked
Boolean
Whether the checkbox is initially checked. Default: false.
style
CheckboxStyle?
Custom styling for the checkbox appearance (colors, text styles, etc.).
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import com.pxp.checkout.styles.CheckboxStyle

@Composable
fun SubscriptionCheckout(pxpCheckout: PxpCheckout) {
    // Create consent component
    val consentComponent = remember {
        pxpCheckout.createComponent<GooglePayConsentComponent, GooglePayConsentConfig>(
            type = ComponentType.GOOGLE_PAY_CONSENT,
            config = GooglePayConsentConfig(
                label = "Save this payment method for my monthly subscription",
                initialChecked = false,
                style = CheckboxStyle(
                    checkedColor = Color(0xFF4CAF50),
                    checkedLabelStyle = TextStyle(
                        fontWeight = FontWeight.Bold,
                        color = Color(0xFF4CAF50)
                    )
                )
            )
        )
    }
    
    // Create Google Pay button with consent
    val googlePayComponent = remember {
        pxpCheckout.createComponent<GooglePayButtonComponent, GooglePayButtonComponentConfig>(
            type = ComponentType.GOOGLE_PAY_BUTTON,
            config = GooglePayButtonComponentConfig().apply {
                googlePayConsentComponent = consentComponent
                // ... payment configuration
            }
        )
    }
    
    // Render components
    Column {
        googlePayComponent.Content()
        Spacer(modifier = Modifier.height(16.dp))
        consentComponent.Content()
    }
}

Charging subsequent recurring payments

After the initial setup with Google Pay, you initiate subsequent recurring charges from your backend using the PXP Transactions API with the stored gatewayTokenId.

POST
/v1/transactions

Example API request:

{
  "merchant": "MERCHANT-1",
  "site": "SITE-1",
  "merchantTransactionId": "recurring-charge-456",
  "merchantTransactionDate": "2025-03-15T10:30:00.000Z",
  "transactionMethod": {
    "intent": "Purchase",
    "entryType": "Ecom",
    "fundingType": "Card"
  },
  "fundingData": {
    "card": {
      "gatewayTokenId": "5fbd77ce-02c1-40ed-94bc-1016660b7512"
    }
  },
  "amounts": {
    "transaction": 9.99,
    "currencyCode": "GBP"
  },
  "recurring": {
    "processingModel": "MerchantInitiatedSubsequentRecurring"
  }
}
Parameter Description
fundingData.card.gatewayTokenId
String
The token ID obtained from the initial Google Pay payment (stored on your backend after the initial authorisation).
recurring.processingModel
String
Must be "MerchantInitiatedSubsequentRecurring" for recurring charges after the initial setup.
transactionMethod.intent
String
Use "Purchase" for immediate charge or "Authorisation" for pre-authorisation.

Example API response:

{
  "state": "Captured",
  "provider": {
    "code": "00",
    "message": "Successful",
    "transactionId": "TXN-987654321"
  },
  "fundingData": {
    "card": {
      "cardScheme": "VISA",
      "expiryMonth": "12",
      "expiryYear": "2026",
      "lastFour": "4242",
      "gatewayTokenId": "5fbd77ce-02c1-40ed-94bc-1016660b7512"
    }
  }
}

The SDK automatically sets processingModel to "MerchantInitiatedInitialRecurring" for the initial Google Pay transaction. For subsequent charges, you must use the Transactions API from your backend with "MerchantInitiatedSubsequentRecurring".

For more details on the Transactions API, see Initiate transactions.

Complete implementation example

Here's a complete example of implementing recurring payments with Google Pay:

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pxp.PxpCheckout
import com.pxp.checkout.components.googlepay.*
import com.pxp.checkout.components.googlepay.types.*
import com.pxp.checkout.components.googlepayconsent.*
import com.pxp.checkout.models.*
import com.pxp.checkout.services.models.transaction.Shopper
import com.pxp.checkout.types.ComponentType
import java.text.SimpleDateFormat
import java.util.*

class SubscriptionActivity : ComponentActivity() {
    
    private lateinit var pxpCheckout: PxpCheckout
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Initialise SDK
        pxpCheckout = PxpCheckout.builder()
            .withConfig(
                PxpSdkConfig(
                    environment = Environment.TEST,
                    session = sessionData,
                    ownerId = "your-owner-id",
                    ownerType = "MerchantGroup",
                    transactionData = TransactionData(
                        currency = "GBP",
                        amount = 9.99,
                        merchantTransactionId = UUID.randomUUID().toString(),
                        merchantTransactionDate = { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(Date()) },
                        entryType = EntryType.Ecom,
                        intent = TransactionIntentData(card = IntentType.Authorisation),
                        merchant = "Merchant Name",
                        recurring = RecurringData(
                            frequencyInDays = 30,
                            frequencyExpiration = "2027-12-31T00:00:00Z"
                        )
                ),
                onGetShopper = {
                    Shopper(id = "customer-123")
                }
                )
            )
            .withContext(this)
            .build()
        
        setContent {
            SubscriptionScreen(pxpCheckout)
        }
    }
}

@Composable
fun SubscriptionScreen(pxpCheckout: PxpCheckout) {
    // Create consent component
    val consentComponent = remember {
        pxpCheckout.createComponent<GooglePayConsentComponent, GooglePayConsentConfig>(
            type = ComponentType.GOOGLE_PAY_CONSENT,
            config = GooglePayConsentConfig(
                label = "Save my payment method for monthly subscription payments"
            )
        )
    }
    
    // Create Google Pay button
    val googlePayComponent = remember {
        pxpCheckout.createComponent<GooglePayButtonComponent, GooglePayButtonComponentConfig>(
            type = ComponentType.GOOGLE_PAY_BUTTON,
            config = GooglePayButtonComponentConfig().apply {
                paymentDataRequest = PaymentDataRequest(
                    allowedPaymentMethods = listOf(
                        PaymentMethodSpecification(
                            parameters = PaymentMethodParameters(
                                allowedCardNetworks = listOf(
                                    CardNetwork.VISA,
                                    CardNetwork.MASTERCARD
                                ),
                                allowedAuthMethods = listOf(
                                    CardAuthMethod.PAN_ONLY,
                                    CardAuthMethod.CRYPTOGRAM_3DS
                                )
                            )
                        )
                    ),
                    transactionInfo = TransactionInfo(
                        currencyCode = "GBP",
                        totalPriceStatus = TotalPriceStatus.FINAL,
                        totalPrice = "9.99"
                    )
                )
                
                googlePayConsentComponent = consentComponent
                
                onPostAuthorisation = { result, paymentData ->
                    when (result) {
                        is AuthorizedSubmitResult -> {
                            Log.d("Subscription", "Subscription authorized: ${result.providerResponse.code}")
                            // Store subscription details
                        }
                        is CapturedSubmitResult -> {
                            Log.d("Subscription", "Subscription captured: ${result.providerResponse.code}")
                            // Store subscription details
                        }
                        is MerchantSubmitResult -> {
                            Log.d("Subscription", "Subscription created: ${result.merchantTransactionId}")
                        }
                        is FailedSubmitResult -> {
                            Log.e("Subscription", "Payment failed: ${result.errorReason}")
                        }
                    }
                }
            }
        )
    }
    
    // Render UI
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text("Premium Subscription", style = MaterialTheme.typography.headlineMedium)
        Text("£9.99/month", style = MaterialTheme.typography.titleLarge)
        Spacer(modifier = Modifier.height(24.dp))
        googlePayComponent.Content()
        Spacer(modifier = Modifier.height(16.dp))
        consentComponent.Content()
    }
}

Processing recurring payments

Backend implementation

Your backend must handle the initial token creation and subsequent recurring charges. Here's the flow:

When the first payment is completed, your webhook receives:

{
  "transactionId": "txn_123",
  "customerId": "customer-123",
  "amount": 9.99,
  "currency": "GBP",
  "consentToken": "tok_abc123",
  "frequencyInDays": 30,
  "frequencyExpiration": "2027-12-31T00:00:00Z"
}

Store the consentToken securely in your database for future use.

Step 2: Recurring charge

To process subsequent payments, make a server-to-server API call to the PXP API using the stored consent token:

// Server-side code (not SDK)
POST /api/v1/transactions
Authorization: Bearer your-api-key

{
  "amount": 9.99,
  "currency": "GBP",
  "customerId": "customer-123",
  "consentToken": "tok_abc123",
  "merchantTransactionId": "recurring_456",
  "description": "Monthly subscription - February 2024"
}

Scheduled billing

Implement a scheduled job to process recurring payments:

// Example server-side scheduler
fun scheduleRecurringPayments() {
    val subscriptions = getActiveSubscriptions()
    
    subscriptions.forEach { subscription ->
        if (subscription.isPaymentDue()) {
            processRecurringPayment(
                customerId = subscription.customerId,
                consentToken = subscription.consentToken,
                amount = subscription.amount,
                currency = subscription.currency
            )
        }
    }
}

Managing subscriptions

Updating payment methods

Customers can update their payment method by making a new payment with consent:

val updatePaymentConfig = GooglePayButtonComponentConfig().apply {
    googlePayConsentComponent = consentComponent
    
    onPostAuthorisation = { result, paymentData ->
        when (result) {
            is AuthorizedSubmitResult -> {
                // Retrieve and update stored consent token from backend
                val transactionDetails = fetchTransactionDetails(result.providerResponse.code)
                
                updateSubscriptionPaymentMethod(
                    customerId = "customer-123",
                    newConsentToken = transactionDetails.consentToken
                )
            }
            is CapturedSubmitResult -> {
                // Retrieve and update stored consent token from backend
                val transactionDetails = fetchTransactionDetails(result.providerResponse.code)
                
                updateSubscriptionPaymentMethod(
                    customerId = "customer-123",
                    newConsentToken = transactionDetails.consentToken
                )
            }
            is MerchantSubmitResult -> {
                // Retrieve and update stored consent token from backend
                val transactionDetails = fetchTransactionDetails(result.systemTransactionId)
                
                updateSubscriptionPaymentMethod(
                    customerId = "customer-123",
                    newConsentToken = transactionDetails.consentToken
                )
            }
            is FailedSubmitResult -> {
                showError("Failed to update payment method")
            }
        }
    }
}

Cancelling subscriptions

Allow customers to cancel their subscriptions:

fun cancelSubscription(customerId: String) {
    // Delete or deactivate the consent token
    deleteConsentToken(customerId)
    
    // Update subscription status
    updateSubscriptionStatus(customerId, SubscriptionStatus.CANCELLED)
}

Handling failed recurring payments

Implement retry logic for failed recurring payments:

fun handleFailedRecurringPayment(
    subscription: Subscription,
    failureReason: String
) {
    when {
        subscription.retryCount < 3 -> {
            // Retry with exponential backoff
            scheduleRetry(
                subscription = subscription,
                delayDays = 2.pow(subscription.retryCount)
            )
        }
        else -> {
            // Suspend subscription after 3 failed attempts
            suspendSubscription(subscription)
            notifyCustomer(subscription, FailureReason.PAYMENT_FAILURE)
        }
    }
}

Test scenarios

Test these key scenarios:

  1. Initial payment with consent: Verify consent token is created and stored.
  2. Recurring payment: Test automated recurring charges using stored token.
  3. Failed recurring payment: Test retry logic and customer notifications.
  4. Update payment method: Verify customers can update their payment details.
  5. Cancel subscription: Test subscription cancellation flow.

Test implementation

@Test
fun testRecurringPaymentFlow() {
    // Step 1: Initial payment with consent
    val initialResult = processInitialPayment(
        amount = 9.99,
        withConsent = true
    )
    
    assert(initialResult.consentToken != null)
    
    // Step 2: Process recurring payment
    val recurringResult = processRecurringPayment(
        consentToken = initialResult.consentToken,
        amount = 9.99
    )
    
    assert(recurringResult.isSuccess)
}