Skip to content

Apple Pay

Enable secure, biometric-authenticated checkout on Safari and iOS devices.

Overview

Apple Pay is automatically included in your drop-in when enabled in your session and when the customer's device supports it. The drop-in handles all Apple Pay setup, button rendering, and payment processing automatically.

Key benefits

  • Automatic detection: Only shows on Safari/iOS with Apple Pay configured
  • Native experience: Uses device's native Apple Pay sheet with Face ID/Touch ID
  • Zero configuration: Drop-in handles merchant IDs and tokenisation
  • Unified callbacks: Same onSuccess/onError as other payment methods
  • Fastest checkout: Customers tap, authenticate, and complete their payment (typically under 3 seconds)

How it works

  1. The customer clicks "Apple Pay" in the accordion. A native payment sheet appears (iOS/macOS).
  2. The customer authenticates with Face ID, Touch ID, or passcode.
  3. The customer approves the payment. The payment sheet closes automatically.
  4. The payment is processed through Unity.
  5. Your onSuccess callback fires.

Configuration

Configure Apple Pay-specific settings and callbacks for dynamic pricing updates.

Configuration properties

The following properties are available for Apple Pay configuration:

Property Description
shippingContactConfiguration
ApplePayShippingContactConfiguration
Configuration for collecting the shipping address. Should be used with billingContactConfiguration
billingContactConfiguration
ApplePayBillingContactConfiguration
Configuration for collecting the billing address. Should be used with shippingContactConfiguration
onShippingContactSelected
(contact) => Promise<ApplePayShippingContactUpdate>
Called when a shipping contact is selected by the customer or when Apple Pay automatically applies the user's default shipping address
onShippingMethodSelected
(method) => Promise<ApplePayShippingMethodUpdate>
Called when a shipping method is selected by the customer
onPaymentMethodSelected
(paymentMethod) => Promise<ApplePayPaymentMethodUpdate>
Called when a payment method is selected by the customer or when Apple Pay automatically selects a default payment method (for example, the top card in the user's wallet)
onCouponCodeChanged
(couponCode) => Promise<ApplePayCouponCodeUpdate>
Called when coupon code is entered

Callbacks may be invoked even when the user doesn't explicitly perform an action. This can occur when Apple Pay applies default selections.

Contact fields and pre-filling

Apple Pay allows you to collect contact information (shipping and billing addresses) and optionally pre-fill this information for the customer. For example, you can pre-fill a logged-in user's saved address from your database, or leave the fields empty to let Apple Pay use the customer's default address from their wallet.

The following properties are available for contact configuration:

Property Description
requireShippingContactFields
ApplePayContactField[]
Array of required shipping fields that the customer must provide (e.g., ['email', 'phone', 'name', 'postalAddress']). Defaults to [].
shippingContact
ApplePayPaymentContact
Pre-filled shipping contact information shown by default. The customer can modify these values. Defaults to undefined.
requireBillingContactFields
ApplePayContactField[]
Array of required billing fields that the customer must provide (e.g., ['postalAddress', 'name']). Defaults to [].
billingContact
ApplePayPaymentContact
Pre-filled billing contact information shown by default. The customer can modify these values. Defaults to undefined.

Available contact fields

  • email - Email address
  • phone - Phone number
  • name - Full name
  • postalAddress - Postal/shipping address
  • phoneticName - Phonetic name (for Japanese and Chinese)

Contact object properties

When pre-filling contact information using shippingContact or billingContact, the following properties are available:

Property Description
givenName
string
First name (e.g., 'John')
familyName
string
Last name (e.g., 'Doe')
emailAddress
string
Email address (e.g., 'john.doe@example.com')
phoneNumber
string
Phone number with country code (e.g., '+1234567890')
addressLines
string[]
Street address lines (e.g., ['123 Main St', 'Apt 4B'])
locality
string
City (e.g., 'New York')
administrativeArea
string
State/Province code (e.g., 'NY', 'CA')
postalCode
string
ZIP/Postal code (e.g., '10001')
country
string
Country name (e.g., 'United States')
countryCode
string
ISO country code (e.g., 'US')
phoneticGivenName
string
Phonetic first name (for Japanese/Chinese names)
phoneticFamilyName
string
Phonetic last name (for Japanese/Chinese names)

Example

This example shows how to require specific fields and pre-fill contact information for a logged-in user:

applePay: {
  // Shipping contact configuration
  shippingContactConfiguration: {
    // REQUIRED: Fields the customer must provide
    requireShippingContactFields: ['email', 'phone', 'name', 'postalAddress'],
    
    // OPTIONAL: Pre-fill shipping information (customer can modify)
    shippingContact: {
      givenName: 'John',
      familyName: 'Doe',
      emailAddress: 'john.doe@example.com',
      phoneNumber: '+1234567890',
      addressLines: ['123 Main St', 'Apt 4B'],
      locality: 'New York',
      administrativeArea: 'NY',
      postalCode: '10001',
      country: 'United States',
      countryCode: 'US'
    }
  },
  
  // Billing contact configuration
  billingContactConfiguration: {
    // REQUIRED: Fields the customer must provide for AVS verification
    requireBillingContactFields: ['postalAddress', 'name'],
    
    // OPTIONAL: Pre-fill billing information (customer can modify)
    billingContact: {
      givenName: 'John',
      familyName: 'Doe',
      addressLines: ['123 Main St'],
      locality: 'New York',
      administrativeArea: 'NY',
      postalCode: '10001',
      country: 'United States',
      countryCode: 'US'
    }
  }
}

Key points

  • Required fields: When you specify requireShippingContactFields or requireBillingContactFields, Apple Pay enforces that the customer provides these fields before payment can be completed.
  • Pre-filled data: The shippingContact and billingContact objects provide default values that appear in the payment sheet, saving the customer time. The customer can still modify these values.
  • Validation: Even if you pre-fill data, the customer can change it. Use the onShippingContactSelected callback to validate the final address before completing payment.

Summary

The following table summarises how contact configuration properties work:

ConfigurationPurposeEffect
requireShippingContactFields: ['email', 'phone', 'name', 'postalAddress']Specify required fieldsCustomer must provide these fields to complete payment
shippingContact: { givenName: 'John', ... }Provide default valuesThese values appear pre-filled in the payment sheet (customer can modify)
Both togetherRequire fields + pre-fillCustomer sees pre-filled data but must ensure all required fields are complete

Settings used from global configuration

Apple Pay inherits the following settings from methodConfig.global:

Property Description
acceptedCardNetworks
string[]
Which card brands to accept through Apple Pay (falls back to session.allowedFundingTypes.cardSchemes)
allowedCardFundingSource
string[]
Which funding types to accept (Credit, Debit, Prepaid)
transactionInfo.countryCode
string
Country code for the transaction (falls back to Unity Portal merchant country: siteConfig.merchantConfiguration.countryCode)
transactionInfo.merchantDisplayName
string
Merchant name shown in Apple Pay sheet (falls back to Unity Portal merchant name: siteConfig.merchantConfiguration.merchantName)
transactionInfo
object
Other transaction display information (totalLabel, lineItems, etc.)
shippingOptions
array
Available shipping options
onGetConsent
(paymentMethod) => boolean
Controls whether to show consent checkbox for Apple Pay
onCancel
(paymentMethod, data) => void
Called when user cancels Apple Pay payment (dismisses payment sheet)

Complete example

This example demonstrates a full Apple Pay configuration with global settings, contact configuration, and all callback implementations:

methodConfig: {
  global: {
    // Apple Pay uses these settings
    acceptedCardNetworks: ['Visa', 'Mastercard', 'American Express'],
    allowedCardFundingSource: ['CREDIT', 'DEBIT'],
    transactionInfo: {
      countryCode: 'US',
      merchantDisplayName: 'Demo Store',
      totalLabel: 'Total Amount',
      totalStatus: 'FINAL',
      lineItems: [
        { label: 'Subtotal', amount: '95.00' },
        { label: 'Tax', amount: '5.00' }
      ]
    },
    shippingOptions: [
      { id: 'ground', label: 'Ground Shipping', amount: '5.00', description: 'Arrives in 5-7 days' },
      { id: 'express', label: 'Express Shipping', amount: '15.00', description: 'Arrives in 2-3 days' }
    ]
  },
  applePay: {
    // Shipping contact configuration
    shippingContactConfiguration: {
      // Require specific shipping fields from the customer
      requireShippingContactFields: ['email', 'phone', 'name', 'postalAddress']
    },
    
    // Billing contact configuration (optional)
    billingContactConfiguration: {
      // Require specific billing fields from the customer
      requireBillingContactFields: ['postalAddress', 'name']
    },
    
    // Called when shipping contact is selected
    onShippingContactSelected: async (contact) => {
      console.log('Shipping contact selected:', contact);
      
      // Update pricing based on shipping location
      return {
        newTotal: {
          label: 'Total',
          amount: '105.00',
          type: 'final'
        },
        newLineItems: [
          { label: 'Subtotal', amount: '95.00', type: 'final' },
          { label: 'Shipping', amount: '10.00', type: 'final' }
        ]
      };
    },
    
    // Called when shipping method is selected
    onShippingMethodSelected: async (method) => {
      console.log('Shipping method selected:', method);
      
      // Update total based on shipping method
      const shippingCost = method.identifier === 'express' ? 15.00 : 5.00;
      const newTotal = 95.00 + shippingCost;
      
      // Optional: Update session on backend for consistency
      // await updateSessionOnBackend(sessionData.sessionId, {
      //   amounts: { transactionValue: newTotal, currencyCode: 'USD' }
      // });
      
      return {
        newTotal: {
          label: 'Total',
          amount: newTotal.toFixed(2),
          type: 'final'
        },
        newLineItems: [
          { label: 'Subtotal', amount: '95.00', type: 'final' },
          { label: 'Shipping', amount: shippingCost.toFixed(2), type: 'final' }
        ]
      };
    },
    
    // Called when payment method (card type) is selected
    onPaymentMethodSelected: async (paymentMethod) => {
      console.log('Payment method selected:', paymentMethod);
      
      // Add credit card processing fee for credit cards
      const creditCardFee = paymentMethod.type === 'credit' ? 2.50 : 0;
      
      return {
        newTotal: {
          label: 'Total',
          amount: (100.00 + creditCardFee).toFixed(2),
          type: 'final'
        },
        newLineItems: [
          { label: 'Subtotal', amount: '100.00', type: 'final' },
          ...(creditCardFee > 0 ? [{ label: 'Credit Card Fee', amount: creditCardFee.toFixed(2), type: 'final' }] : [])
        ]
      };
    },
    
    // Called when coupon code is entered
    onCouponCodeChanged: async (couponCode) => {
      console.log('Coupon code entered:', couponCode);
      
      // Validate coupon and apply discount
      const coupons: Record<string, number> = {
        'SAVE10': 10.00,
        'SAVE20': 20.00,
        'WELCOME5': 5.00
      };
      
      const discount = coupons[couponCode.toUpperCase()] || 0;
      
      return {
        newTotal: {
          label: 'Total',
          amount: (100.00 - discount).toFixed(2),
          type: 'final'
        },
        newLineItems: [
          { label: 'Subtotal', amount: '100.00', type: 'final' },
          ...(discount > 0 ? [{ label: 'Discount', amount: (-discount).toFixed(2), type: 'final' }] : [])
        ]
      };
    }
  }
}

Apple Pay requirements

Apple Pay requires the following to function correctly:

  • HTTPS: Your website must be served over HTTPS. Apple Pay doesn't work on HTTP connections.
  • Domain verification: Your domain must be registered and verified in the Unity Portal.
  • Merchant certificate: Apple Pay merchant certificate must be configured in the Unity Portal.
  • Browser compatibility: Apple Pay is supported on Safari and Safari WebView. It works on:
    • Safari on macOS
    • Safari on iOS (iPhone 6 or later, iPad Air 2 or later)
    • Safari WebView in native iOS apps
  • Customer setup: The customer must have at least one card added to their Apple Wallet.

Implementation

Apple Pay works through the standard implementation, with no Apple Pay-specific code needed for basic usage:

import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';

// Get session from backend (with Apple Pay enabled)
const sessionData = await fetch('/api/create-session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
}).then(response => response.json());

// Initialise Drop-in
const checkoutDropIn = CheckoutDropIn.initialize({
  environment: 'test',
  session: sessionData, // Must include applePay in allowedFundingTypes.wallets
  ownerId: 'MERCHANT-1',
  ownerType: 'MerchantGroup',
  transactionData: {
    currency: 'USD',
    amount: 99.99,
    entryType: 'Ecom',
    intent: {
      card: IntentType.Authorisation,
      paypal: IntentType.Authorisation
    },
    merchantTransactionId: crypto.randomUUID(),
    merchantTransactionDate: () => new Date().toISOString()
  },
  onGetShopper: () => Promise.resolve({ id: 'shopper-001' }),
  onSuccess: async (result: BaseSubmitResult) => {
    console.log('Apple Pay payment successful!');
    console.log('System transaction ID:', result.systemTransactionId);
    console.log('Payment method:', result.paymentMethod); // Will be "ApplePay"
    
    // CRITICAL: Verify on backend
    await verifyPaymentOnBackend(result);
  },
  onError: (error: BaseSdkException) => {
    console.error('Apple Pay error:', error);
    
    // Handle specific error scenarios
    if (error.ErrorCode === 'SDK0615') {
      // User cancelled Apple Pay - don't show error
      console.log('User cancelled Apple Pay');
    } else if (error.ErrorCode === 'SDK1119') {
      // Apple Pay payment failed
      alert('Apple Pay payment failed. Please try a different payment method.');
    } else if (error.ErrorCode === 'SDK0601') {
      // Apple Pay not available
      alert('Apple Pay is not available on this device or browser.');
    } else if (error.message.toLowerCase().includes('declined')) {
      alert('Payment declined. Please try a different payment method.');
    } else {
      alert(`Payment failed: ${error.message}`);
    }
  }
});

// Mount Drop-in
checkoutDropIn.create('checkout-drop-in-container');

Session configuration (backend)

Enable Apple Pay in your session request:

// BACKEND: Create session with Apple Pay enabled
const sessionRequest = {
  merchant: "MERCHANT-1",
  site: "SITE-1",
  sessionTimeout: 120,
  merchantTransactionId: crypto.randomUUID(),
  transactionMethod: {
    intent: {
      card: "Authorisation"
    }
  },
  amounts: {
    currencyCode: "USD",
    transactionValue: 99.99
  },
  allowedFundingTypes: {
    card: true,
    wallets: {
      applePay: true  // Enable Apple Pay
    }
  },
  allowTransaction: true,
  serviceType: "CheckoutDropIn"
};

Handling responses

Apple Pay callback data

When an Apple Pay payment succeeds, your onSuccess callback receives the same standard result as other payment methods:

onSuccess: (result: BaseSubmitResult) => {
  console.log('Payment Details:');
  console.log('- System Transaction ID:', result.systemTransactionId);
  console.log('- Merchant Transaction ID:', result.merchantTransactionId);
  console.log('- Payment Method:', result.paymentMethod); // "ApplePay"
  
  // Note: Amount, currency, and card details must be retrieved from backend
  // Apple Pay tokenises cards - actual card details are not exposed
}

Error handling

Handle Apple Pay-specific errors:

import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';

onError: (error: BaseSdkException) => {
  // Use error.ErrorCode for SDK-defined errors
  if (error.ErrorCode === 'SDK0615') {
    // Apple Pay session was cancelled by the user
    console.log('User cancelled Apple Pay');
    // Don't show error - user intentionally closed payment sheet
  } else if (error.ErrorCode === 'SDK1119') {
    // Apple Pay payment failed
    alert('Apple Pay payment failed. Please try a different payment method.');
  } else if (error.ErrorCode === 'SDK0601') {
    // Apple Pay not available
    alert('Apple Pay is not available on this device or browser.');
  } else if (error.message.toLowerCase().includes('declined')) {
    alert('Payment declined. Please try a different payment method.');
  } else if (error.message.toLowerCase().includes('not set up') || 
             error.message.toLowerCase().includes('wallet')) {
    alert('Apple Pay is not set up. Please add a card to Apple Wallet.');
  } else if (error.message.toLowerCase().includes('restricted')) {
    alert('Apple Pay is restricted on this device.');
  } else {
    alert(`Payment failed: ${error.message}`);
  }
}

Backend verification

Always verify Apple Pay payments on your backend to ensure payment success before fulfilling orders:

import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';

onSuccess: async (result: BaseSubmitResult) => {
  // Send to backend for verification
  const verified = await fetch('/api/verify-payment', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      systemTransactionId: result.systemTransactionId,
      merchantTransactionId: result.merchantTransactionId
    })
  }).then(r => r.json());
  
  if (verified.success) {
    window.location.href = `/success?orderId=${verified.orderId}`;
  } else {
    alert('Payment verification failed');
  }
}

Backend verification code

Use the following backend code to verify Apple Pay transactions via the PXP API:

// BACKEND: Verify Apple Pay payment
app.post('/api/verify-payment', async (req, res) => {
  const { systemTransactionId, merchantTransactionId } = req.body;
  
  try {
    // Query PXP API to get transaction details
    const txnPath = `api/v1/transactions/${systemTransactionId}`;
    const { authHeader, requestId } = createAuthHeader(
      txnPath,
      '',
      process.env.PXP_TOKEN_ID,
      process.env.PXP_TOKEN_VALUE
    );
    
    const transaction = await fetch(
      `https://api-services.pxp.io/${txnPath}`,
      {
        headers: {
          'X-Client-Id': process.env.PXP_CLIENT_ID,
          'X-Request-Id': requestId,
          'Authorization': authHeader
        }
      }
    ).then(r => r.json());
    
    // Verify transaction state
    if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
      return res.json({ success: false, error: 'Transaction not successful' });
    }
    
    // Verify merchant transaction ID matches
    if (transaction.merchantTransactionId !== merchantTransactionId) {
      return res.json({ success: false, error: 'Transaction ID mismatch' });
    }
    
    // Verify amount matches expected amount from your order records
    const order = await getOrderByMerchantTransactionId(merchantTransactionId);
    const txnAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
    if (Math.abs(txnAmount - order.amount) > 0.01) {
      return res.json({ success: false, error: 'Amount mismatch' });
    }
    
    // Apple Pay payments show as Card funding type in PXP API
    const fundingType = transaction.fundingData?.fundingType || 
                       transaction.fundingType || 
                       'Unknown';
    if (fundingType !== 'Card') {
      return res.json({ success: false, error: 'Invalid funding type' });
    }
    
    // Fulfill order
    const orderId = await fulfillOrder(transaction);
    
    return res.json({ success: true, orderId });
    
  } catch (error) {
    console.error('Verification error:', error);
    return res.json({ success: false, error: 'Verification failed' });
  }
});

Recurring payments

Drop-in supports Apple Pay recurring payments for subscription-based services and merchant-initiated transactions (MITs). Configure the recurring object in your transaction data to set up the initial payment, then use the stored gatewayTokenId for subsequent charges.

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

Drop-in doesn't support Apple Pay's native recurring payment features (recurringPaymentRequest, deferredPaymentRequest, or automaticReloadPaymentRequest). For those features, use the Apple Pay button Ccmponent instead. Drop-in uses the standard recurring object approach, which works across all payment methods.

How recurring payments work

The recurring payment flow involves two phases:

Phase 1: Initial setup (via Drop-in)

  1. Configure the recurring object in transactionData with frequency and expiration.
  2. The customer authenticates with Face ID/Touch ID and completes the Apple Pay payment.
  3. Drop-in automatically sets processingModel to MerchantInitiatedInitialRecurring.
  4. The backend stores the gatewayTokenId from the transaction response.

When you include the recurring object in your transaction data, Drop-in automatically sets the processingModel to MerchantInitiatedInitialRecurring for the initial transaction.

Phase 2: Subsequent charges (via backend API)

  1. Your scheduling system triggers a charge based on the configured frequency.
  2. Your backend calls the Transactions API using the stored gatewayTokenId.
  3. Set processingModel to MerchantInitiatedSubsequentRecurring for subsequent charges.
  4. The payment processes without customer interaction.

Initial recurring payment setup

Configure recurring payment frequency in your Drop-in initialisation:

import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';
import { BaseSubmitResult } from '@pxpio/web-components-sdk/src/checkoutDropIn/types/BaseSubmitResult';
import BaseSdkException from '@pxpio/web-components-sdk/src/types/sdkExceptions/BaseSdkException';

// Get session from backend (with Apple Pay enabled)
const response = await fetch('/api/create-session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }
});

const result = await response.json();
const sessionData = result.data;

// Initialise Drop-in with Apple Pay recurring configuration
const checkoutDropIn = CheckoutDropIn.initialize({
  environment: 'test',
  session: sessionData,
  ownerId: 'MERCHANT-1',
  ownerType: 'MerchantGroup',
  transactionData: {
    currency: 'USD',
    amount: 9.99,
    entryType: 'Ecom',
    intent: {
      card: IntentType.Authorisation,
      paypal: IntentType.Authorisation
    },
    merchantTransactionId: crypto.randomUUID(),
    merchantTransactionDate: () => new Date().toISOString(),
    // Recurring configuration for processing model
    recurring: {
      frequencyInDays: 30,           // Bill every 30 days
      frequencyExpiration: '2025-12-31' // Token expires on this date
    }
  },
  onGetShopper: () => Promise.resolve({ id: 'customer-123' }),
  methodConfig: {
    global: {
      transactionInfo: {
        countryCode: 'US',
        merchantDisplayName: 'Subscription Service',
        totalLabel: 'Monthly Subscription',
        totalStatus: 'FINAL',
        lineItems: [
          { label: 'Premium Plan', amount: '9.99' }
        ]
      }
    },
    applePay: {
      // No specific recurring configuration needed for Drop-in
      // Drop-in uses the standard recurring object above
    }
  },
  onSuccess: async (result: BaseSubmitResult) => {
    console.log('Apple Pay recurring payment setup completed');
    
    // Verify and store gatewayTokenId on backend
    const response = await fetch('/api/verify-and-setup-recurring', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        systemTransactionId: result.systemTransactionId,
        merchantTransactionId: result.merchantTransactionId,
        paymentMethod: 'ApplePay'
      })
    }).then(r => r.json());
    
    if (response.success) {
      window.location.href = `/subscription-success?subscriptionId=${response.subscriptionId}`;
    }
  },
  onError: (error: BaseSdkException) => {
    if (error.ErrorCode === 'SDK0615') {
      console.log('User cancelled Apple Pay');
      return;
    }
    console.error('Apple Pay subscription setup failed:', error);
    alert(`Subscription setup failed: ${error.message}`);
  }
});

checkoutDropIn.create('checkout-drop-in-container');

Backend: Store recurring payment token

After the initial payment, extract and store the gatewayTokenId:

// BACKEND: Verify Apple Pay payment and set up recurring subscription
app.post('/api/verify-and-setup-recurring', async (req, res) => {
  const { systemTransactionId, merchantTransactionId, paymentMethod } = req.body;
  
  try {
    // Query PXP API to get transaction details
    const txnPath = `api/v1/transactions/${systemTransactionId}`;
    const { authHeader, requestId } = createAuthHeader(
      txnPath,
      '',
      process.env.PXP_TOKEN_ID,
      process.env.PXP_TOKEN_VALUE
    );
    
    const transaction = await fetch(
      `https://api-services.pxp.io/${txnPath}`,
      {
        headers: {
          'X-Client-Id': process.env.PXP_CLIENT_ID,
          'X-Request-Id': requestId,
          'Authorization': authHeader
        }
      }
    ).then(r => r.json());
    
    // Verify transaction is successful
    if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
      return res.json({ success: false, error: 'Transaction not successful' });
    }
    
    // Store gatewayTokenId for recurring charges
    if (transaction.fundingData?.gatewayTokenId) {
      const subscription = await database.subscriptions.insert({
        shopperId: req.user.shopperId,
        gatewayTokenId: transaction.fundingData.gatewayTokenId,
        paymentMethod: paymentMethod,
        amount: transaction.amounts?.transactionValue || 0,
        currency: transaction.amounts?.currencyCode || 'USD',
        frequencyInDays: 30,
        nextChargeDate: calculateNextChargeDate(30),
        status: 'active',
        createdAt: new Date()
      });
      
      return res.json({ success: true, subscriptionId: subscription.id });
    }
    
    return res.json({ success: false, error: 'No gateway token found' });
    
  } catch (error) {
    console.error('Setup error:', error);
    return res.json({ success: false, error: 'Setup failed' });
  }
});

Charging subsequent recurring payments

Use the Transactions API to charge subsequent payments:

POST
/v1/transactions
// BACKEND: Charge subsequent recurring payment
async function chargeRecurringPayment(subscription) {
  const requestBody = {
    merchant: "MERCHANT-1",
    site: "SITE-1",
    merchantTransactionId: `recurring-${Date.now()}`,
    merchantTransactionDate: new Date().toISOString(),
    transactionMethod: {
      intent: "Purchase",
      entryType: "Ecom",
      fundingType: "Card"
    },
    fundingData: {
      card: {
        gatewayTokenId: subscription.gatewayTokenId
      }
    },
    amounts: {
      transaction: subscription.amount,
      currencyCode: subscription.currency
    },
    recurring: {
      processingModel: "MerchantInitiatedSubsequentRecurring"
    }
  };
  
  const path = 'api/v1/transactions';
  const { authHeader, requestId } = createAuthHeader(
    path,
    JSON.stringify(requestBody),
    process.env.PXP_TOKEN_ID,
    process.env.PXP_TOKEN_VALUE
  );
  
  const response = await fetch(
    `https://api-services.pxp.io/${path}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Client-Id': process.env.PXP_CLIENT_ID,
        'X-Request-Id': requestId,
        'Authorization': authHeader
      },
      body: JSON.stringify(requestBody)
    }
  );
  
  const result = await response.json();
  
  if (result.state === 'Captured' || result.state === 'Authorised') {
    await database.subscriptions.update(subscription.id, {
      lastChargeDate: new Date(),
      nextChargeDate: calculateNextChargeDate(subscription.frequencyInDays),
      lastTransactionId: result.systemTransactionId
    });
    
    console.log('Recurring charge successful:', result.systemTransactionId);
    return { success: true, transactionId: result.systemTransactionId };
  } else {
    console.error('Recurring charge failed:', result);
    return { success: false, error: result.errorReason };
  }
}

Learn more about initiating transactions via API.