Learn how to implement recurring payment tokens with Google Pay for subscriptions and recurring billing.
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:
- The customer makes their first payment via Google Pay.
- During payment, the customer grants consent for future charges.
- The SDK creates and authorises the initial payment.
- The backend stores encrypted payment token for future use.
- The customer receives confirmation of payment and consent.
- Your backend initiates recurring payments using the stored
gatewayTokenIdvia the PXP Transactions API. - The payment processes without customer interaction.
- The customer receives notification of the charge.
- The token remains valid for future transactions until expiration.
To implement recurring payments, you need to:
- Configure the SDK with
recurringdata intransactionData. - Use either the
google-pay-consentcomponent or theonGetConsentcallback. - Provide a shopper ID via
onGetShopperfor consent tracking.
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 |
|---|---|
frequencyInDaysInt | The billing frequency in days (e.g., 30 for monthly, 365 for annual). |
frequencyExpirationString? | 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 |
|---|---|
labelString | The consent text displayed to the customer. |
initialCheckedBoolean | Whether the checkbox is initially checked. Default: false. |
styleCheckboxStyle? | 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()
}
}After the initial setup with Google Pay, you initiate subsequent recurring charges from your backend using the PXP Transactions API with the stored gatewayTokenId.
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.gatewayTokenIdString | The token ID obtained from the initial Google Pay payment (stored on your backend after the initial authorisation). |
recurring.processingModelString | Must be "MerchantInitiatedSubsequentRecurring" for recurring charges after the initial setup. |
transactionMethod.intentString | 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.
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()
}
}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.
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"
}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
)
}
}
}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")
}
}
}
}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)
}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 these key scenarios:
- Initial payment with consent: Verify consent token is created and stored.
- Recurring payment: Test automated recurring charges using stored token.
- Failed recurring payment: Test retry logic and customer notifications.
- Update payment method: Verify customers can update their payment details.
- Cancel subscription: Test subscription cancellation flow.
@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)
}