Skip to content

Events

Implement callbacks to customise your Drop-in payment flow.

Overview

Drop-in provides a unified event system that works consistently across all payment methods. You can use these callbacks to inject your own business logic and user experience customisations into the payment flow at critical moments. They ensure that while the SDK handles the complex technical aspects of payment processing, you retain full control over the customer experience and can seamlessly integrate payments into your broader business workflows and systems.

Callbacks enable you to:

  • Validate business rules before payments proceed.
  • Display custom loading states and progress indicators.
  • Show user-friendly error messages.
  • Tailor user interfaces to match your brand's look and feel.
  • Integrate with your own systems for fraud detection or customer management.
  • Control exactly how your customers experience both successful and failed transactions.
  • Track analytics and monitor payment performance.
  • Handle shipping calculations and address validation in real-time (PayPal, Apple Pay, Google Pay).
  • Implement card-on-file functionality for returning customers.
  • Offer alternative payment methods when errors occur.

Your callbacks receive normalised data regardless of whether the customer paid with cards, PayPal, Google Pay, or Apple Pay.

All events are optional except onGetShopper, which is required for card-on-file functionality.

Supported events

onGetShopper

This callback is required to provide shopper information for card-on-file functionality, allowing returning customers to use saved payment methods.

You can use it to:

  • Provide shopper ID for logged-in users.
  • Generate anonymous shopper ID for guest checkout.
  • Enable card-on-file for returning customers.
  • Associate payment methods with customer accounts.

Event data

This callback receives no parameters and must return a Promise<Shopper>.

Return value

PropertyDescription
id
string
Unique shopper identifier. Use customer ID for logged-in users or generate a unique ID for guests.

Example implementation

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  onGetShopper: () => {
    // For logged-in users, return their customer ID
    const customerId = getCurrentCustomerId();
    if (customerId) {
      return Promise.resolve({
        id: customerId
      });
    }
    
    // For guest users, generate or retrieve a session-based ID
    const guestId = getOrCreateGuestId();
    return Promise.resolve({
      id: guestId
    });
  }
});

This callback is required for Checkout Drop-in initialisation. The shopper ID is used to store and retrieve saved payment methods. For guests, generate a unique ID (e.g., UUID) that persists across the session. For registered users, use their customer/account ID from your system.

onBeforeSubmit

This callback is triggered when a payment method is selected and the user is about to submit payment. This is your last chance to validate data or cancel the payment before it's processed.

You can use it to:

  • Perform final validation before payment submission.
  • Check inventory availability.
  • Verify customer account status.
  • Display confirmation dialogs.
  • Track analytics events.
  • Perform fraud checks.

Event data

ParameterDescription
before
any
Payment selection details containing information about the selected payment method.

Return value

  • true or undefined: Continue with payment submission.
  • false: Cancel payment submission.
  • Promise<boolean>: Async validation (resolved value determines whether to proceed).

Example implementation

import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  onBeforeSubmit: async (before) => {
    console.log('Payment method selected:', before);
    
    // Track analytics
    analytics.track('payment_initiated', {
      paymentMethod: before
    });
    
    // Show confirmation dialog
    const confirmed = await showConfirmationDialog({
      title: 'Confirm Payment',
      message: 'Proceed with payment?',
      confirmText: 'Confirm',
      cancelText: 'Cancel'
    });
    
    if (!confirmed) {
      console.log('Payment cancelled by user');
      return false; // Cancel payment
    }
    
    // Check inventory availability
    const inventoryCheck = await fetch('/api/check-inventory', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ items: getCartItems() })
    }).then(r => r.json());
    
    if (!inventoryCheck.available) {
      alert('Some items are no longer available. Please review your cart.');
      return false; // Cancel payment
    }
    
    // Verify customer account is in good standing
    const accountCheck = await fetch('/api/verify-account', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ customerId: getCurrentCustomerId() })
    }).then(r => r.json());
    
    if (!accountCheck.valid) {
      alert('Account verification failed. Please contact support.');
      return false; // Cancel payment
    }
    
    console.log('All checks passed, proceeding with payment');
    return true; // Continue with payment
  }
});

Simple validation example:

onBeforeSubmit: (before) => {
  // Simple synchronous validation
  if (!isValidOrder()) {
    alert('Please complete all required fields');
    return false; // Payment will not proceed
  }
  
  return true; // Payment will proceed
}

onSubmit

This callback is triggered when payment processing begins, after onBeforeSubmit validation passes. Use this to show loading indicators and disable UI elements.

You can use it to:

  • Show loading spinners or progress indicators.
  • Disable submit buttons to prevent double-submission.
  • Display "Processing payment..." messages.
  • Update UI to show payment is in progress.
  • Track analytics for payment submission.

Event data

ParameterDescription
submit
any
Payment submission details containing information about the payment being processed.

Example implementation

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  onSubmit: (submit) => {
    console.log('Processing payment with:', submit);
    
    // Show loading overlay
    const overlay = document.getElementById('loading-overlay');
    if (overlay) {
      overlay.style.display = 'flex';
      overlay.querySelector('.message').textContent = 'Processing payment...';
    }
    
    // Disable submit button to prevent double-submission
    const submitBtn = document.querySelector('button[type="submit"]');
    if (submitBtn) {
      submitBtn.disabled = true;
      submitBtn.textContent = 'Processing...';
    }
    
    // Track analytics
    analytics.track('payment_processing', {
      paymentMethod: submit
    });
  }
});

Complete loading state management:

let loadingTimeout;

onSubmit: (submit) => {
  // Show loading state
  setLoadingState(true);
  
  // Set timeout in case payment takes too long
  loadingTimeout = setTimeout(() => {
    showWarning('Payment is taking longer than expected. Please wait...');
  }, 10000); // 10 seconds
  
  // Track start time for performance monitoring
  window.paymentStartTime = Date.now();
}

onSuccess

This callback is triggered after payment succeeds. It receives the final transaction result from the payment processing system.

You can use it to:

  • Verify payment on your backend (REQUIRED).
  • Redirect to success page after verification.
  • Display success messages.
  • Track successful payment analytics.
  • Update stock levels for purchased items.
  • Send order confirmation emails to customers.
  • Clear shopping cart.

Event data

Event dataDescription
result
BaseSubmitResult
The payment processing result from PXP's backend.
result.systemTransactionId
string
Unity's system transaction identifier. Use for backend verification.
result.merchantTransactionId
string | undefined
Your unique transaction identifier (optional).
result.paymentMethod
PaymentMethod
The payment method used: PaymentMethod.Card, PaymentMethod.Paypal, PaymentMethod.GooglePay, or PaymentMethod.ApplePay.

BaseSubmitResult type:

interface BaseSubmitResult {
  systemTransactionId: string;
  merchantTransactionId?: string;  // Optional
  paymentMethod: PaymentMethod;
}

While authenticationId is available in some payment responses, you should always retrieve and verify authentication details on your backend by querying the PXP API with the systemTransactionId for secure, production-ready implementations.

The onSuccess callback is a frontend event and can be manipulated by malicious users. Never fulfill orders based solely on this callback. Always verify payments on your backend using Unity webhooks or the Query Transaction API before fulfilling orders.

Example implementation

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

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  onSuccess: async (result: BaseSubmitResult) => {
    console.log('Payment successful (frontend notification only)');
    console.log('System transaction ID:', result.systemTransactionId);
    console.log('Merchant transaction ID:', result.merchantTransactionId);
    console.log('Payment method:', result.paymentMethod);
    
    // Clear loading timeout if set
    if (loadingTimeout) {
      clearTimeout(loadingTimeout);
    }
    
    // Hide loading overlay
    setLoadingState(false);
    
    // Track analytics
    const processingTime = Date.now() - window.paymentStartTime;
    analytics.track('payment_success_frontend', {
      systemTransactionId: result.systemTransactionId,
      merchantTransactionId: result.merchantTransactionId,
      paymentMethod: result.paymentMethod,
      processingTime: processingTime
    });
    
    // Show temporary success message
    showMessage('Payment received! Verifying...', 'success');
    
    // CRITICAL: Verify payment on backend before fulfilling order
    try {
      const verification = await fetch('/api/verify-payment', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          systemTransactionId: result.systemTransactionId,
          merchantTransactionId: result.merchantTransactionId
        })
      }).then(response => {
        if (!response.ok) {
          throw new Error('Verification request failed');
        }
        return response.json();
      });
      
      if (verification.success) {
        // Payment verified on backend - safe to proceed
        console.log('Payment verified on backend:', verification.orderId);
        
        // Track backend verification success
        analytics.track('payment_verified', {
          orderId: verification.orderId,
          systemTransactionId: result.systemTransactionId
        });
        
        // Clear cart
        clearShoppingCart();
        
        // Redirect to success page
        window.location.href = `/order-confirmation?orderId=${verification.orderId}`;
      } else {
        // Verification failed
        console.error('Backend verification failed:', verification.error);
        
        analytics.track('payment_verification_failed', {
          systemTransactionId: result.systemTransactionId,
          error: verification.error
        });
        
        alert(
          'Payment verification failed. Please contact support with ' +
          `transaction ID: ${result.merchantTransactionId}`
        );
      }
    } catch (error) {
      // Network or server error during verification
      console.error('Failed to verify payment:', error);
      
      analytics.track('payment_verification_error', {
        systemTransactionId: result.systemTransactionId,
        error: error.message
      });
      
      // Show error but don't clear cart (webhook may still process it)
      alert(
        'Unable to verify payment. Your payment may still be processing. ' +
        'Please check your email for confirmation or contact support with ' +
        `transaction ID: ${result.merchantTransactionId}`
      );
    }
  }
});

Backend verification endpoint example:

// Node.js/Express backend
app.post('/api/verify-payment', async (req, res) => {
  const { systemTransactionId, merchantTransactionId } = req.body;
  
  // Retrieve expected amount/currency from your order database
  const order = await db.orders.findOne({ merchantTransactionId });
  if (!order) {
    return res.json({ success: false, error: 'Order not found' });
  }
  
  const expectedAmount = order.amount;
  const expectedCurrency = order.currency;
  
  try {
    // Check if webhook already processed this transaction
    const existingTransaction = await db.transactions.findOne({
      systemTransactionId: systemTransactionId
    });
    
    if (existingTransaction) {
      // Webhook already processed - return order details
      return res.json({
        success: true,
        orderId: existingTransaction.orderId,
        source: 'webhook'
      });
    }
    
    // Webhook hasn't arrived yet - query PXP API as fallback
    const requestPath = `api/v1/transactions?systemTransactionId=${systemTransactionId}`;
    const requestBody = '';
    const { authHeader, requestId } = createAuthHeader(
      requestPath, 
      requestBody, 
      process.env.PXP_TOKEN_ID, 
      process.env.PXP_TOKEN_VALUE
    );
    
    const response = await fetch(
      `https://api-services.pxp.io/${requestPath}`,
      {
        headers: {
          'X-Client-Id': process.env.PXP_CLIENT_ID,
          'X-Request-Id': requestId,
          'Authorization': authHeader
        }
      }
    );
    
    if (!response.ok) {
      throw new Error('PXP API request failed');
    }
    
    const transaction = await response.json();
    
    // Verify transaction details
    if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
      return res.json({ success: false, error: 'Transaction not authorised' });
    }
    
    const txnAmount = transaction.amounts?.transactionValue || transaction.amount || 0;
    if (Math.abs(txnAmount - expectedAmount) > 0.01) {
      return res.json({ success: false, error: 'Amount mismatch' });
    }
    
    if (transaction.merchantTransactionId !== merchantTransactionId) {
      return res.json({ success: false, error: 'Transaction ID mismatch' });
    }
    
    // For 3DS card transactions, authenticationId is in the transaction response
    if (transaction.authenticationId) {
      const authPath = `api/v1/threedsecure/integrated/authentications/${transaction.authenticationId}`;
      const { authHeader: auth3dsHeader, requestId: auth3dsRequestId } = createAuthHeader(
        authPath,
        '',
        process.env.PXP_TOKEN_ID,
        process.env.PXP_TOKEN_VALUE
      );
      
      const authResponse = await fetch(
        `https://api-services.pxp.io/${authPath}`,
        {
          headers: {
            'X-Client-Id': process.env.PXP_CLIENT_ID,
            'X-Request-Id': auth3dsRequestId,
            'Authorization': auth3dsHeader
          }
        }
      );
      
      const authResult = await authResponse.json();
      
      if (authResult.transactionStatus !== 'Y' && authResult.transactionStatus !== 'A') {
        return res.json({ success: false, error: '3DS authentication failed' });
      }
    }
    
    // Find order
    const order = await db.orders.findOne({
      transactionId: merchantTransactionId
    });
    
    if (!order) {
      return res.json({ success: false, error: 'Order not found' });
    }
    
    // Record transaction
    const recordedAmount = transaction.amounts?.transactionValue || transaction.amount;
    await db.transactions.create({
      systemTransactionId,
      merchantTransactionId,
      orderId: order.id,
      amount: recordedAmount,
      state: transaction.state,
      processedAt: new Date(),
      source: 'api_fallback'
    });
    
    // Fulfill order
    await fulfillOrder(order.id, systemTransactionId);
    
    res.json({
      success: true,
      orderId: order.id,
      source: 'api'
    });
  } catch (error) {
    console.error('Payment verification error:', error);
    res.status(500).json({ success: false, error: 'Verification failed' });
  }
});

onError

This callback is triggered when an error occurs during the payment process.

You can use it to:

  • Display user-friendly error messages.
  • Log errors for debugging and monitoring.
  • Track failed payment analytics.
  • Offer alternative payment methods.
  • Provide retry options.
  • Hide loading indicators.
  • Re-enable form controls.

Event data

ParameterDescription
error
BaseSdkException
The error object containing details about what went wrong.
error.message
string
Human-readable error message.
error.code
string
SDK error code in format SDK#### (e.g., 'SDK1114' for authentication failed, 'SDK0500' for network error). Use this for programmatic error handling.

BaseSdkException type:

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

Common error scenarios

The following table shows common error scenarios and how to detect and respond to them:

ScenarioDetection approachUser action
Card declinederror.message contains "declined" or specific provider messagesTry a different card.
Insufficient fundserror.message contains "insufficient funds"Use a different payment method.
Expired carderror.message contains "expired"Use a different card.
Invalid CVVerror.message contains "CVV" or "security code"Check security code.
3DS authentication failederror.code === 'SDK1114' or error.message contains "Authentication failed"Try again or use different card.
3DS timeouterror.message contains "timeout"Check connection and retry.
User cancelled 3DSerror.message contains "cancel"Retry payment.
PayPal errorerror.code === 'SDK1117' or payment method is PayPalTry again or use different method.
Session expirederror.message contains "session" or "expired"Refresh page and retry.
Network errorerror.code === 'SDK0500'Check connection and retry.
Configuration errorerror.code starts with 'SDK01' or 'SDK02'Contact support.

Example implementation

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

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  onError: (error: BaseSdkException) => {
    console.error('Payment failed');
    console.error('Error code:', error.code);
    console.error('Error message:', error.message);
    
    // Clear loading timeout if set
    if (loadingTimeout) {
      clearTimeout(loadingTimeout);
    }
    
    // Hide loading overlay
    setLoadingState(false);
    
    // Re-enable submit button
    const submitBtn = document.querySelector('button[type="submit"]');
    if (submitBtn) {
      submitBtn.disabled = false;
      submitBtn.textContent = 'Pay Now';
    }
    
    // Track error analytics
    analytics.track('payment_failed', {
      errorCode: error.code,
      errorMessage: error.message
    });
    
    // Log error for monitoring
    logErrorToMonitoring({
      category: 'payment_error',
      code: error.code,
      message: error.message,
      userAgent: navigator.userAgent
    });
    
    // Show user-friendly error message based on error code and message
    let userMessage = 'Payment failed. Please try again or contact support.';
    
    // Check by SDK error code
    if (error.code === 'SDK0500') {
      userMessage = 'Network connection issue. Please check your internet connection and try again.';
    } else if (error.code === 'SDK1114') {
      userMessage = '3D Secure authentication failed. Please try again or use a different card.';
    } else if (error.code === 'SDK1116') {
      userMessage = 'Card payment failed. Please check your card details and try again.';
    } else if (error.code === 'SDK1117') {
      userMessage = 'PayPal payment failed. Please try again or use a different payment method.';
    } else if (error.code === 'SDK1118') {
      userMessage = 'Google Pay payment failed. Please try again or use a different payment method.';
    } else if (error.code === 'SDK1119') {
      userMessage = 'Apple Pay payment failed. Please try again or use a different payment method.';
    }
    // Check by message content for provider-specific errors
    else if (error.message.toLowerCase().includes('declined')) {
      userMessage = 'Your card was declined. Please try a different card or contact your bank.';
    } else if (error.message.toLowerCase().includes('insufficient funds')) {
      userMessage = 'Insufficient funds. Please use a different payment method.';
    } else if (error.message.toLowerCase().includes('expired')) {
      userMessage = 'This card has expired. Please use a different card.';
    } else if (error.message.toLowerCase().includes('cvv') || error.message.toLowerCase().includes('security code')) {
      userMessage = 'Invalid security code. Please check your CVV and try again.';
    } else if (error.message.toLowerCase().includes('timeout')) {
      userMessage = 'Request timed out. Please check your internet connection and try again.';
    } else if (error.message.toLowerCase().includes('cancel')) {
      userMessage = 'Payment was cancelled. Please try again to complete your payment.';
    } else if (error.message.toLowerCase().includes('session') || error.message.toLowerCase().includes('expired')) {
      userMessage = 'Your payment session has expired. Please refresh the page and try again.';
    }
    
    showErrorMessage(userMessage);
    
    // Offer alternatives for card errors
    if (error.code === 'SDK1116' || error.message.toLowerCase().includes('declined') || 
        error.message.toLowerCase().includes('insufficient funds')) {
      showAlternativePaymentOptions();
    }
    
    // Offer retry for network errors
    if (error.code === 'SDK0500' || error.message.toLowerCase().includes('network') || 
        error.message.toLowerCase().includes('timeout')) {
      showRetryButton();
    }
  }
});

Error handling with retry logic:

let retryCount = 0;
const MAX_RETRIES = 3;

onError: (error) => {
  // Check if error is retryable (network issues, timeouts)
  const isNetworkError = error.code === 'SDK0500';
  const isTimeout = error.message.toLowerCase().includes('timeout');
  const isRetryable = isNetworkError || isTimeout;
  
  if (isRetryable && retryCount < MAX_RETRIES) {
    retryCount++;
    
    console.log(`Retryable error, attempt ${retryCount}/${MAX_RETRIES}`);
    
    showMessage(
      `Connection issue. Attempt ${retryCount}/${MAX_RETRIES}. ` +
      'Please try your payment again.',
      'warning'
    );
  } else {
    retryCount = 0; // Reset retry count
    
    if (isRetryable) {
      showMessage(
        'Unable to process payment after multiple attempts. ' +
        'Please check your connection and try again later.',
        'error'
      );
    } else {
      showMessage(
        'Payment failed: ' + error.message,
        'error'
      );
    }
    
    // Show alternative options for persistent failures
    showAlternativePaymentMethods();
  }
}

### onGetConsent

This callback controls whether to show a consent checkbox for a specific payment method. This is typically used to get user permission to save payment information for future use (card-on-file).

You can use it to:
* Show consent checkbox only for specific payment methods.
* Comply with regulations requiring explicit consent for storing payment data.
* Allow users to opt-in to card-on-file functionality.
* Control consent display based on user type (guest vs registered).

#### Event data


#### Return value

* `true`: Show consent checkbox for this payment method.
* `false`: Hide consent checkbox for this payment method.

#### Example implementation
```typescript
const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  methodConfig: {
    global: {
      // Control consent checkbox for card-on-file
      onGetConsent: (paymentMethod: PaymentMethod) => {
        // Show consent for cards and PayPal, but not for wallet payments
        if (paymentMethod === PaymentMethod.Card || paymentMethod === PaymentMethod.Paypal) {
          return true;
        }
        return false;
      }
    }
  }
});

Dynamic consent based on user type:

onGetConsent: (paymentMethod: PaymentMethod) => {
  // Only show consent for logged-in users
  const isLoggedIn = checkUserLoginStatus();
  
  if (!isLoggedIn) {
    return false; // Guest users cannot save payment methods
  }
  
  // Show consent for cards only
  return paymentMethod === PaymentMethod.Card;
}

### onCancel

This callback is triggered when a payment is cancelled by the user. This allows you to track cancellations, update UI, or perform cleanup operations.

You can use it to:
* Track payment abandonment analytics.
* Show user-friendly cancellation message.
* Re-enable form fields or buttons.
* Clear loading indicators.
* Offer alternative payment methods.
* Send abandonment emails for cart recovery.

#### Event data


This callback is triggered when:

* User closes PayPal popup without completing payment.
* User cancels Apple Pay payment sheet.
* User cancels Google Pay payment sheet.
* User closes 3D Secure authentication window.
* User explicitly cancels the payment flow.

#### Example implementation
```typescript
const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  methodConfig: {
    global: {
      onCancel: (paymentMethod: PaymentMethod, data: any) => {
        console.log('Payment cancelled:', paymentMethod);
        
        // Track analytics
        analytics.track('payment_cancelled', {
          method: paymentMethod,
          timestamp: Date.now()
        });
        
        // Hide loading spinner
        setIsProcessing(false);
        
        // Re-enable checkout button
        setCheckoutButtonDisabled(false);
        
        // Show cancellation message
        showNotification({
          type: 'info',
          message: `${paymentMethod} payment was cancelled. Please try again or choose a different payment method.`
        });
        
        // Reset payment form
        resetPaymentForm();
      }
    }
  }
});

Event data structures

PaymentMethod enum

The PaymentMethod enum identifies which payment method is being used:

enum PaymentMethod {
  Card = 'Card',
  Paypal = 'Paypal',
  ApplePay = 'ApplePay',
  GooglePay = 'GooglePay'
}

Import from:

import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';

BaseSubmitResult interface

The success callback receives a BaseSubmitResult object:

interface BaseSubmitResult {
  systemTransactionId: string;      // Unity's transaction ID
  merchantTransactionId?: string;   // Your transaction ID (optional)
  paymentMethod: PaymentMethod;     // Payment method used
}

Import from:

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

BaseSdkException interface

The error callback receives a BaseSdkException object:

interface BaseSdkException {
  message: string;      // Human-readable error message
  ErrorCode: string;    // SDK error code (e.g., 'SDK1114')
}

Import from:

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

Shopper interface

The onGetShopper callback must return a Shopper object:

interface Shopper {
  id: string;  // Unique shopper identifier
}

Error handling in events

Event callbacks should handle errors gracefully and provide appropriate feedback to customers. Here's a comprehensive error handling example:

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... other config
  
  onError: (error: BaseSdkException) => {
    // Log error for debugging
    console.error('Payment error:', error.code, error.message);
    
    // Clear any loading states
    hideLoadingIndicator();
    
    // Track error for analytics
    analytics.track('payment_error', {
      code: error.code,
      message: error.message,
      timestamp: Date.now()
    });
    
    // Categorize error type
    let errorCategory = 'unknown';
    let userMessage = 'Payment failed. Please try again.';
    let showAlternatives = false;
    
    // Network and timeout errors
    if (error.code === 'SDK0500' || error.message.toLowerCase().includes('network')) {
      errorCategory = 'network';
      userMessage = 'Network connection issue. Please check your internet and try again.';
    }
    // Card-specific errors
    else if (error.code === 'SDK1116' || error.message.toLowerCase().includes('card')) {
      errorCategory = 'card';
      if (error.message.toLowerCase().includes('declined')) {
        userMessage = 'Card declined. Please try a different card.';
        showAlternatives = true;
      } else if (error.message.toLowerCase().includes('insufficient funds')) {
        userMessage = 'Insufficient funds. Please use a different payment method.';
        showAlternatives = true;
      } else if (error.message.toLowerCase().includes('expired')) {
        userMessage = 'This card has expired. Please use a different card.';
      } else if (error.message.toLowerCase().includes('cvv')) {
        userMessage = 'Invalid security code. Please check your CVV.';
      }
    }
    // 3DS authentication errors
    else if (error.code === 'SDK1114' || error.message.toLowerCase().includes('authentication')) {
      errorCategory = '3ds';
      userMessage = '3D Secure authentication failed. Please try again.';
    }
    // Wallet payment errors
    else if (error.code === 'SDK1117') {
      errorCategory = 'paypal';
      userMessage = 'PayPal payment failed. Please try again or use a different method.';
    } else if (error.code === 'SDK1118') {
      errorCategory = 'googlepay';
      userMessage = 'Google Pay payment failed. Please try again or use a different method.';
    } else if (error.code === 'SDK1119') {
      errorCategory = 'applepay';
      userMessage = 'Apple Pay payment failed. Please try again or use a different method.';
    }
    // Session errors
    else if (error.message.toLowerCase().includes('session') || error.message.toLowerCase().includes('expired')) {
      errorCategory = 'session';
      userMessage = 'Your session has expired. Please refresh the page.';
    }
    
    // Display error message to user
    showErrorMessage(userMessage);
    
    // Show alternative payment methods if appropriate
    if (showAlternatives) {
      showAlternativePaymentMethodsPrompt();
    }
    
    // Log to monitoring service
    logToMonitoring({
      level: 'error',
      category: errorCategory,
      code: error.code,
      message: error.message,
      context: {
        userAgent: navigator.userAgent,
        timestamp: new Date().toISOString()
      }
    });
  }
});

Advanced event usage

Payment method-specific callbacks

Checkout Drop-in supports advanced callbacks for dynamic pricing, shipping updates, and payment customisation. These callbacks are configured within methodConfig for each payment method.

PayPal callbacks (in methodConfig.paypal):

  • onShippingAddressChange - Triggered when shipping address changes in the PayPal popup.
  • onShippingOptionsChange - Triggered when shipping option changes in the PayPal popup.

Apple Pay callbacks (in methodConfig.applePay):

  • onShippingContactSelected - Triggered when shipping contact is selected in the Apple Pay sheet.
  • onShippingMethodSelected - Triggered when shipping method is selected in the Apple Pay sheet.
  • onPaymentMethodSelected - Triggered when payment method (card type) is selected in the Apple Pay sheet.
  • onCouponCodeChanged - Triggered when coupon code is entered in the Apple Pay sheet (iOS 15.0+).

Google Pay callbacks (in methodConfig.googlePay):

  • onPaymentDataChanged - Triggered when payment data changes in the Google Pay sheet.

For detailed documentation on these advanced callbacks, see:

Callback execution order

Understanding the callback flow helps you implement proper payment handling:

  1. onGetShopper - Initial setup

    • Called during Drop-in initialisation.
    • Must return shopper ID for card-on-file functionality.
  2. onBeforeSubmit - Validation phase

    • User selects payment method and clicks submit.
    • Receives payment selection details as parameter.
    • Return true to proceed, false to cancel.
    • Perform business validation and fraud checks.
  3. onSubmit - Processing starts (if onBeforeSubmit returns true or undefined)

    • Receives payment submission details as parameter.
    • Show loading indicators.
    • Disable form fields.
    • Track analytics.
  4. onSuccess OR onError - Payment completes

    • onSuccess - Payment succeeded (verify on backend before fulfilling).
    • onError - Payment failed (show error message and offer alternatives).
  5. onCancel - User cancels (optional)

    • Triggered if user closes payment window/sheet.
    • Update UI and track analytics.

Execution flow diagram

Initialisation

onGetShopper() → Returns shopper ID

User selects payment method and clicks submit

onBeforeSubmit(before) → Returns true/false

    ├─ false → Payment cancelled, flow stops
    └─ true  → Continue

       onSubmit(submit) → Show loading state

       Payment processing

           ├─ Success → onSuccess(result) → Verify on backend → Redirect
           ├─ Error   → onError(error) → Show error message
           └─ Cancel  → onCancel(paymentMethod, data) → Reset UI

Complete integration example

Here's a complete example showing all callbacks working together:

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';
import PaymentMethod from '@pxpio/web-components-sdk/src/components/checkoutDropInComponents/types/PaymentMethod';

let loadingTimeout;
let retryCount = 0;
const MAX_RETRIES = 3;

async function initializeCheckout() {
  // Get session from backend
  const sessionData = await fetch('/api/create-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: 25.00, currency: 'USD' })
  }).then(r => r.json());
  
  // Initialise Drop-in with all callbacks
  const checkoutDropIn = CheckoutDropIn.initialize({
    environment: 'production',
    session: sessionData,
    ownerId: 'YourMerchantId',
    ownerType: 'MerchantGroup',
    transactionData: {
      currency: 'USD',
      amount: 25.00,
      entryType: 'Ecom',
      intent: {
        card: IntentType.Authorisation,
        paypal: IntentType.Authorisation
      },
      merchantTransactionId: crypto.randomUUID(),
      merchantTransactionDate: () => new Date().toISOString()
    },
    
    // Required: Get shopper information
    onGetShopper: () => {
      const customerId = getCurrentCustomerId();
      return Promise.resolve({
        id: customerId || 'guest-' + crypto.randomUUID()
      });
    },
    
    // Before payment submission
    onBeforeSubmit: async (before) => {
      console.log('Payment method selected:', before);
      
      // Track analytics
      analytics.track('payment_initiated', {
        paymentMethod: before,
        amount: 25.00
      });
      
      // Show confirmation for large amounts
      if (25.00 > 100) {
        const confirmed = await showConfirmation(
          `Confirm payment of 25.00 USD?`
        );
        if (!confirmed) return false;
      }
      
      // Validate inventory
      const hasStock = await checkInventory();
      if (!hasStock) {
        alert('Some items are no longer available');
        return false;
      }
      
      return true; // Proceed with payment
    },
    
    // Payment processing started
    onSubmit: (submit) => {
      console.log('Processing payment...');
      
      // Show loading state
      document.getElementById('loading-overlay').style.display = 'flex';
      document.getElementById('submit-btn').disabled = true;
      
      // Set timeout warning
      loadingTimeout = setTimeout(() => {
        showMessage('Payment is taking longer than expected...', 'warning');
      }, 10000);
      
      // Track processing start
      window.paymentStartTime = Date.now();
      
      analytics.track('payment_processing', {
        paymentMethod: submit
      });
    },
    
    // Payment succeeded (frontend notification)
    onSuccess: async (result: BaseSubmitResult) => {
      console.log('Payment successful (verifying on backend...)');
      
      // Clear timeout
      if (loadingTimeout) clearTimeout(loadingTimeout);
      
      // Calculate processing time
      const processingTime = Date.now() - window.paymentStartTime;
      console.log(`Payment processed in ${processingTime}ms`);
      
      // Track frontend success
      analytics.track('payment_success_frontend', {
        systemTransactionId: result.systemTransactionId,
        paymentMethod: result.paymentMethod,
        processingTime
      });
      
      // Show verifying message
      showMessage('Payment received! Verifying...', 'success');
      
      // CRITICAL: Verify on backend
      try {
        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) {
          // Backend verification passed
          analytics.track('payment_verified', {
            orderId: verified.orderId
          });
          
          // Clear cart and redirect
          clearCart();
          window.location.href = `/success?orderId=${verified.orderId}`;
        } else {
          throw new Error(verified.error || 'Verification failed');
        }
      } catch (error) {
        console.error('Verification error:', error);
        document.getElementById('loading-overlay').style.display = 'none';
        alert(
          'Payment verification failed. Please contact support with ' +
          `transaction ID: ${result.merchantTransactionId}`
        );
      }
    },
    
    // Payment failed
    onError: (error: BaseSdkException) => {
      console.error('Payment failed:', error.code);
      console.error('Error message:', error.message);
      
      // Clear timeout
      if (loadingTimeout) clearTimeout(loadingTimeout);
      
      // Hide loading state
      document.getElementById('loading-overlay').style.display = 'none';
      document.getElementById('submit-btn').disabled = false;
      
      // Track error
      analytics.track('payment_failed', {
        errorCode: error.code,
        errorMessage: error.message
      });
      
      // Log for monitoring
      logError(error);
      
      // Handle retryable errors
      const isNetworkError = error.code === 'SDK0500';
      const isTimeout = error.message.toLowerCase().includes('timeout');
      if ((isNetworkError || isTimeout) && retryCount < MAX_RETRIES) {
        retryCount++;
        showMessage(
          `Connection issue (attempt ${retryCount}/${MAX_RETRIES}). Please try again.`,
          'warning'
        );
        return;
      }
      
      retryCount = 0; // Reset
      
      // Show user-friendly error
      let userMessage = 'Payment failed. Please try again.';
      
      if (error.code === 'SDK1114') {
        userMessage = '3D Secure authentication failed.';
      } else if (error.code === 'SDK1116') {
        userMessage = 'Card payment failed.';
      } else if (error.code === 'SDK1117') {
        userMessage = 'PayPal payment failed.';
      } else if (error.message.toLowerCase().includes('declined')) {
        userMessage = 'Card declined. Please try a different card.';
      } else if (error.message.toLowerCase().includes('insufficient funds')) {
        userMessage = 'Insufficient funds. Please use a different payment method.';
      } else if (error.message.toLowerCase().includes('expired')) {
        userMessage = 'This card has expired.';
      } else if (error.message.toLowerCase().includes('cvv')) {
        userMessage = 'Invalid security code.';
      }
      
      showErrorMessage(userMessage);
      
      // Offer alternatives for card issues
      if (error.code === 'SDK1116' || error.message.toLowerCase().includes('card')) {
        showAlternativePaymentMethods();
      }
    },
    
    // Method-specific configuration
    methodConfig: {
      global: {
        // Control consent checkbox
        onGetConsent: (paymentMethod: PaymentMethod) => {
          // Show consent for cards and PayPal
          return paymentMethod === PaymentMethod.Card || 
                 paymentMethod === PaymentMethod.Paypal;
        },
        
        // Handle cancellation
        onCancel: (paymentMethod: PaymentMethod, data: any) => {
          console.log('Payment cancelled:', paymentMethod);
          
          // Clear timeout
          if (loadingTimeout) clearTimeout(loadingTimeout);
          
          // Hide loading
          document.getElementById('loading-overlay').style.display = 'none';
          document.getElementById('submit-btn').disabled = false;
          
          // Track cancellation
          analytics.track('payment_cancelled', {
            paymentMethod: paymentMethod
          });
          
          showMessage('Payment was cancelled. Please try again.', 'info');
        }
      }
    }
  });
  
  // Mount Drop-in
  checkoutDropIn.create('checkout-drop-in-container');
}

// Initialise on page load
initializeCheckout();