Learn how to implement recurring payment tokens with Google Pay for subscriptions and recurring billing.
Recurring payments enable secure subscriptions and scheduled billing with Google Pay. By configuring the recurring object in your transaction data during the initial payment, you can obtain a reusable payment token for future transactions without requiring the customer to re-authenticate each time.
Recurring payments are ideal 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.
Recurring payments are enabled through the recurring configuration in transactionData. This is separate from the consent component, which is used to collect customer permission to store payment methods. See Configuration for details on the consent component.
The recurring payment flow involves two main phases:
- Customer makes first payment via Google Pay.
- SDK processes the payment with
recurringconfiguration intransactionData. - Backend receives and stores the payment token for future use.
- Customer receives confirmation of payment and recurring setup.
- Merchant initiates recurring payment using stored token via backend API.
- Payment processes without customer interaction.
- Customer receives notification of charge.
- Token remains valid until the
frequencyExpirationdate.
To implement recurring payments, you need to:
- Configure the SDK with
recurringdata intransactionData. - Optionally, collect customer consent using the
google-pay-consentcomponent oronGetConsentcallback (see Configuration). - Provide a shopper ID via
onGetShopperfor tracking.
When initialising the SDK, include the recurring configuration in transactionData:
const pxpSdk = PxpCheckout.initialize({
environment: 'test',
session: sessionData,
ownerId: 'your-owner-id',
ownerType: 'MerchantGroup',
kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
transactionData: {
currency: 'GBP',
amount: 9.99,
merchantTransactionId: crypto.randomUUID(),
merchantTransactionDate: () => new Date().toISOString(),
entryType: 'Ecom',
intent: {
card: 'Authorisation'
},
// Configure recurring payment schedule
recurring: {
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: async () => {
return { id: 'customer-123' };
}
});| Property | Description |
|---|---|
frequencyInDaysnumber | The billing frequency in days (e.g., 30 for monthly, 365 for annual). |
frequencyExpirationstring | The ISO 8601 date string for when the recurring token should expire. |
Configure the Google Pay button with your payment parameters:
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.
const googlePayButton = pxpSdk.create('google-pay-button', {
paymentDataRequest: {
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedCardNetworks: ['VISA', 'MASTERCARD', 'AMEX'],
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS']
}
}],
transactionInfo: {
currencyCode: 'GBP',
totalPriceStatus: 'FINAL',
totalPrice: '9.99'
}
},
// Handle payment authorization
onPostAuthorisation: async (result, paymentData) => {
if (result && 'merchantTransactionId' in result) {
// Success - MerchantSubmitResult
console.log('Payment authorised');
console.log('Transaction ID:', result.merchantTransactionId);
console.log('System Transaction ID:', result.systemTransactionId);
// Store the transaction for subscription tracking
await storeSubscriptionTransaction({
customerId: getCurrentCustomerId(),
transactionId: result.merchantTransactionId,
systemTransactionId: result.systemTransactionId
});
console.log('Subscription transaction recorded');
window.location.href = '/subscription-confirmed';
}
}
});
googlePayButton.mount('google-pay-container');To collect explicit customer consent for storing payment information, use the google-pay-consent component. See Configuration for full documentation.
// Create consent component
const consentComponent = pxpSdk.create('google-pay-consent', {
label: 'I agree to save my payment method for recurring payments',
checked: false
});
// Link consent to Google Pay button
const googlePayButton = pxpSdk.create('google-pay-button', {
paymentDataRequest: {
// ... payment configuration
},
googlePayConsentComponent: consentComponent, // Link the consent
onPostAuthorisation: async (result, paymentData) => {
// Handle payment
}
});
// Mount both components
consentComponent.mount('consent-container');
googlePayButton.mount('google-pay-container');Here's a full example for a subscription service with recurring payments:
import { useEffect, useState } from 'react';
import { PxpCheckout } from '@pxpio/web-components-sdk';
function SubscriptionCheckout() {
const [googlePayButton, setGooglePayButton] = useState(null);
const [subscriptionPlan, setSubscriptionPlan] = useState('monthly');
// Get subscription details
const planDetails = {
monthly: { amount: '9.99', frequency: 'Monthly', frequencyDays: 30 },
annual: { amount: '99.99', frequency: 'Annually', frequencyDays: 365 }
};
const plan = planDetails[subscriptionPlan];
useEffect(() => {
// Initialise PXP SDK with recurring configuration
const pxpSdk = PxpCheckout.initialize({
environment: 'test',
session: sessionData, // Get from your backend
ownerId: 'your-owner-id',
ownerType: 'MerchantGroup',
kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
transactionData: {
currency: 'GBP',
amount: parseFloat(plan.amount),
merchantTransactionId: crypto.randomUUID(),
merchantTransactionDate: () => new Date().toISOString(),
entryType: 'Ecom',
intent: {
card: 'Authorisation'
},
// Configure recurring payment schedule
recurring: {
frequencyInDays: plan.frequencyDays,
frequencyExpiration: getSubscriptionExpiry(plan.frequency)
}
},
onGetShopper: async () => {
// Return customer ID for tracking
return { id: getCurrentCustomerId() };
}
});
// Create Google Pay button
const button = pxpSdk.create('google-pay-button', {
paymentDataRequest: {
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedCardNetworks: ['VISA', 'MASTERCARD', 'AMEX'],
allowedAuthMethods: ['CRYPTOGRAM_3DS'] // Prefer 3DS for subscriptions
}
}],
transactionInfo: {
currencyCode: 'GBP',
countryCode: 'GB',
totalPriceStatus: 'FINAL',
totalPrice: plan.amount,
totalPriceLabel: `${plan.frequency} Subscription`,
displayItems: [
{
label: `Premium ${plan.frequency} Plan`,
type: 'LINE_ITEM',
price: plan.amount,
status: 'FINAL'
}
]
}
},
// Handle initial payment authorisation
onPostAuthorisation: async (result, paymentData) => {
console.log('Payment authorisation result:', result);
if (result && 'merchantTransactionId' in result) {
// Success - MerchantSubmitResult
console.log('✅ Initial subscription payment authorised');
console.log('Transaction ID:', result.merchantTransactionId);
console.log('System Transaction ID:', result.systemTransactionId);
try {
// Create subscription record on your backend
await fetch('/api/subscriptions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: getCurrentCustomerId(),
transactionId: result.merchantTransactionId,
systemTransactionId: result.systemTransactionId,
plan: subscriptionPlan,
amount: plan.amount,
frequency: plan.frequency,
nextBillingDate: getNextBillingDate(plan.frequency),
startDate: new Date().toISOString()
})
});
console.log('✅ Subscription created successfully');
// Redirect to success page
window.location.href = '/subscription/success?plan=' + subscriptionPlan;
} catch (error) {
console.error('❌ Error creating subscription:', error);
showError('An error occurred. Please contact support.');
}
} else if (result && 'errorCode' in result) {
// Failure - FailedSubmitResult
console.error('❌ Payment declined:', result.errorReason);
showError('Payment declined. Please try another payment method.');
}
},
// Handle errors
onError: (error) => {
console.error('Payment error:', error);
showError('An error occurred during payment. Please try again.');
},
// Handle cancellation
onCancel: () => {
console.log('Payment cancelled by user');
showMessage('Subscription setup cancelled');
}
});
// Mount button
button.mount('google-pay-button-container');
setGooglePayButton(button);
// Cleanup on unmount
return () => {
if (button) button.unmount();
};
}, [subscriptionPlan]);
return (
<div className="subscription-checkout">
<h1>Start your subscription</h1>
<div className="plan-selector">
<label>
<input
type="radio"
name="plan"
value="monthly"
checked={subscriptionPlan === 'monthly'}
onChange={(e) => setSubscriptionPlan(e.target.value)}
/>
Monthly - £9.99/month
</label>
<label>
<input
type="radio"
name="plan"
value="annual"
checked={subscriptionPlan === 'annual'}
onChange={(e) => setSubscriptionPlan(e.target.value)}
/>
Annual - £99.99/year (Save 17%)
</label>
</div>
<div id="google-pay-button-container"></div>
<p className="subscription-terms">
By subscribing, you authorise us to charge your payment method {
subscriptionPlan === 'monthly' ? 'monthly' : 'annually'
} until you cancel. See our <a href="/terms">Terms of Service</a>.
</p>
</div>
);
}
// Helper functions
function getCurrentCustomerId() {
// Get current customer ID from your auth system
return localStorage.getItem('customerId') || 'guest-user';
}
function getNextBillingDate(frequency) {
const nextDate = new Date();
if (frequency === 'Monthly') {
nextDate.setMonth(nextDate.getMonth() + 1);
} else if (frequency === 'Annually') {
nextDate.setFullYear(nextDate.getFullYear() + 1);
}
return nextDate.toISOString();
}
function getSubscriptionExpiry(frequency) {
// Set token expiry (e.g., 2 years from now)
const expiry = new Date();
expiry.setFullYear(expiry.getFullYear() + 2);
return expiry.toISOString();
}
function showError(message) {
alert(message); // Replace with your error handling UI
}
function showMessage(message) {
console.log(message); // Replace with your notification system
}
export default SubscriptionCheckout;For collecting explicit customer consent, add the google-pay-consent component. See Configuration for implementation details.
The backend examples in this section are conceptual illustrations showing typical patterns for recurring payment processing. The actual implementation will depend on your backend architecture and the PXP backend integration you are using.
Once you have stored the consent token, use it to process recurring payments:
// Backend API endpoint for processing recurring payment
app.post('/api/subscriptions/charge', async (req, res) => {
const { customerId, subscriptionId } = req.body;
try {
// Retrieve subscription details
const subscription = await getSubscription(subscriptionId);
// Process recurring payment through backend API
// Note: Recurring payment implementation depends on your payment processor
const response = await pxpBackendSDK.payments.create({
amount: subscription.billingAmount,
currency: subscription.billingCurrency,
customerId: customerId,
subscriptionId: subscriptionId,
metadata: {
billingPeriod: getCurrentBillingPeriod(),
paymentType: 'recurring'
}
});
if (response.status === 'Authorised') {
// Update subscription record
await updateSubscriptionLastCharge(subscriptionId, {
transactionId: response.transactionId,
amount: response.amount,
date: new Date().toISOString(),
status: 'success'
});
// Send receipt to customer
await sendPaymentReceipt(customerId, response);
res.json({ success: true, transactionId: response.transactionId });
} else {
// Handle payment failure
await handleRecurringPaymentFailure(subscriptionId, response);
res.status(402).json({ success: false, error: response.errorReason });
}
} catch (error) {
console.error('Recurring payment error:', error);
res.status(500).json({ success: false, error: 'Payment processing failed' });
}
});Implement automated recurring billing:
// Cron job or scheduled task to process recurring payments
async function processScheduledBilling() {
console.log('🔄 Processing scheduled billing...');
// Get subscriptions due for billing
const dueSubscriptions = await getSubscriptionsDueForBilling();
console.log(`Found ${dueSubscriptions.length} subscriptions to process`);
for (const subscription of dueSubscriptions) {
try {
console.log(`Processing subscription ${subscription.id}`);
// Process payment using stored token
const result = await processRecurringPayment(subscription);
if (result.success) {
console.log(`✅ Successfully charged subscription ${subscription.id}`);
// Schedule next billing
await scheduleNextBilling(subscription);
// Send confirmation email
await sendBillingConfirmation(subscription.customerId, result);
} else {
console.error(`❌ Failed to charge subscription ${subscription.id}`);
// Handle failed payment
await handleBillingFailure(subscription, result);
}
} catch (error) {
console.error(`Error processing subscription ${subscription.id}:`, error);
await logBillingError(subscription.id, error);
}
}
console.log('✅ Scheduled billing complete');
}
// Run daily at 2 AM
cron.schedule('0 2 * * *', processScheduledBilling);Allow customers to update their payment method for an existing subscription:
import { PxpCheckout } from '@pxpio/web-components-sdk';
function UpdatePaymentMethod({ subscriptionId }) {
const [googlePayButton, setGooglePayButton] = useState(null);
useEffect(() => {
const pxpSdk = PxpCheckout.initialize({
environment: 'test',
session: sessionData, // Get from your backend
ownerId: 'your-owner-id',
ownerType: 'MerchantGroup',
kountDisabled: false, // OPTIONAL: Set to true to disable Kount fraud detection
transactionData: {
currency: 'GBP',
amount: 0, // No charge for update
merchantTransactionId: crypto.randomUUID(),
merchantTransactionDate: () => new Date().toISOString(),
entryType: 'Ecom',
intent: {
card: 'Verification' // Use Verification for payment method updates
},
recurring: {
frequencyInDays: 30, // Maintain existing frequency
frequencyExpiration: getSubscriptionExpiry()
}
},
onGetShopper: async () => ({ id: getCurrentCustomerId() })
});
const button = pxpSdk.create('google-pay-button', {
paymentDataRequest: {
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedCardNetworks: ['VISA', 'MASTERCARD', 'AMEX'],
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS']
}
}],
transactionInfo: {
currencyCode: 'GBP',
totalPriceStatus: 'FINAL',
totalPrice: '0.00' // No charge for update
}
},
onPostAuthorisation: async (result, paymentData) => {
if (result && 'merchantTransactionId' in result) {
// Success - Payment method update successful
await updateSubscriptionPaymentMethod(subscriptionId, {
transactionId: result.merchantTransactionId,
systemTransactionId: result.systemTransactionId,
updatedAt: new Date().toISOString()
});
alert('Payment method updated successfully');
window.location.href = '/account/subscription';
}
}
});
button.mount('update-payment-button');
setGooglePayButton(button);
return () => {
button.unmount();
};
}, [subscriptionId]);
return (
<div>
<h2>Update payment method</h2>
<p>Add a new payment method for your subscription.</p>
<div id="update-payment-button"></div>
</div>
);
}Handle subscription cancellations:
async function cancelSubscription(subscriptionId, reason) {
try {
// Update subscription status
await fetch(`/api/subscriptions/${subscriptionId}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cancellationReason: reason,
cancelledAt: new Date().toISOString()
})
});
// Note: Consent token should be deleted from your system
// but the token itself remains valid with the payment processor
// until its expiry date. Ensure your backend doesn't use it.
console.log('Subscription cancelled successfully');
// Send cancellation confirmation
await sendCancellationConfirmation(subscriptionId);
return { success: true };
} catch (error) {
console.error('Error cancelling subscription:', error);
return { success: false, error: error.message };
}
}Implement retry logic and customer notifications:
async function handleRecurringPaymentFailure(subscription, result) {
console.error('Recurring payment failed:', result.errorReason);
// Track failure
await recordPaymentFailure(subscription.id, {
reason: result.errorReason,
attemptNumber: subscription.failedAttempts + 1,
date: new Date().toISOString()
});
const failedAttempts = subscription.failedAttempts + 1;
if (failedAttempts === 1) {
// First failure: Retry in 3 days
await scheduleRetry(subscription.id, 3);
await sendPaymentFailureEmail(subscription.customerId, 'first_failure');
} else if (failedAttempts === 2) {
// Second failure: Retry in 5 days
await scheduleRetry(subscription.id, 5);
await sendPaymentFailureEmail(subscription.customerId, 'second_failure');
} else if (failedAttempts >= 3) {
// Third failure: Suspend subscription
await suspendSubscription(subscription.id);
await sendPaymentFailureEmail(subscription.customerId, 'subscription_suspended');
}
}// ✅ DO: Store subscription data securely on backend
await secureStorage.store({
customerId: customer.id,
transactionId: result.merchantTransactionId,
systemTransactionId: result.systemTransactionId,
createdAt: new Date()
});
// ❌ DON'T: Never store sensitive payment data in frontend/localStorage
localStorage.setItem('transactionId', result.merchantTransactionId); // AVOID - use backend storage// Always inform customers before charging
async function notifyUpcomingCharge(subscription) {
const daysBeforeCharge = 3;
await sendEmail(subscription.customerEmail, {
subject: 'Upcoming subscription payment',
body: `Your ${subscription.planName} subscription will renew in ${daysBeforeCharge} days for £${subscription.amount}.`
});
}
// Send receipt after successful charge
async function sendChargeConfirmation(subscription, transaction) {
await sendEmail(subscription.customerEmail, {
subject: 'Payment confirmation',
body: `We've successfully charged £${transaction.amount} for your ${subscription.planName} subscription.`
});
}// Provide clear subscription terms
function SubscriptionTerms({ amount, frequency }) {
return (
<div className="subscription-terms">
<h3>Subscription agreement</h3>
<p>
By completing this purchase, you authorise {merchantName} to:
</p>
<ul>
<li>Charge £{amount} to your payment method {frequency.toLowerCase()}</li>
<li>Store your payment information securely for recurring billing</li>
<li>Continue billing until you cancel your subscription</li>
</ul>
<p>
You can cancel at any time from your account settings.
Cancellations take effect at the end of your current billing period.
</p>
</div>
);
}- Successful initial payment: Verify payment token is created and stored with
recurringconfiguration - Failed initial payment: Ensure no token is stored
- Successful recurring payment: Process payment using stored token via backend API
- Failed recurring payment: Test retry logic and notifications
- Token update: Successfully replace existing payment method
- Subscription cancellation: Verify token is no longer used
// Test mode configuration
const testConfig = {
environment: 'test',
testMode: true,
testCards: {
success: '4111111111111111',
decline: '4000000000000002',
insufficientFunds: '4000000000009995'
}
};
// Verify subscription creation
async function testSubscriptionCreation() {
const result = await processInitialPayment();
assert(result.merchantTransactionId, 'Transaction ID should be present');
assert(result.systemTransactionId, 'System Transaction ID should be present');
const subscription = await getSubscription(result.subscriptionId);
assert(subscription, 'Subscription should be created');
assert(subscription.status === 'active', 'Subscription should be active');
assert(subscription.recurring, 'Recurring configuration should be present');
}Always test the complete subscription lifecycle including initial payment, recurring charges, failed payments, retries, and cancellations before going live.