Skip to content

Recurring payments

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

Overview

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.

How recurring payments work

The recurring payment flow involves two main phases:

Phase 1: Initial payment

  1. Customer makes first payment via Google Pay.
  2. SDK processes the payment with recurring configuration in transactionData.
  3. Backend receives and stores the payment token for future use.
  4. Customer receives confirmation of payment and recurring setup.

Phase 2: Subsequent payments

  1. Merchant initiates recurring payment using stored token via backend API.
  2. Payment processes without customer interaction.
  3. Customer receives notification of charge.
  4. Token remains valid until the frequencyExpiration date.

Implementing recurring payments

To implement recurring payments, you need to:

  1. Configure the SDK with recurring data in transactionData.
  2. Optionally, collect customer consent using the google-pay-consent component or onGetConsent callback (see Configuration).
  3. Provide a shopper ID via onGetShopper for tracking.

Recurring configuration

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' };
  }
});
PropertyDescription
frequencyInDays
number
The billing frequency in days (e.g., 30 for monthly, 365 for annual).
frequencyExpiration
string
The ISO 8601 date string for when the recurring token should expire.

Creating the Google Pay button

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

Complete implementation example

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.

Processing recurring payments

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.

Backend implementation

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

Scheduled billing

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

Managing subscriptions

Updating payment methods

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

Cancelling subscriptions

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

Handling failed recurring payments

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

Best practices

Security

// ✅ 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

Customer communication

// 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.`
  });
}

Compliance

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

Testing recurring payments

Test scenarios

  1. Successful initial payment: Verify payment token is created and stored with recurring configuration
  2. Failed initial payment: Ensure no token is stored
  3. Successful recurring payment: Process payment using stored token via backend API
  4. Failed recurring payment: Test retry logic and notifications
  5. Token update: Successfully replace existing payment method
  6. Subscription cancellation: Verify token is no longer used

Test implementation

// 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.