Skip to content

Troubleshooting

Learn how to diagnose and fix common issues with Checkout Drop-in.

Quick diagnostics

If you're experiencing issues with Drop-in, start with these quick diagnostic checks:

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

// Drop-in diagnostic helper
function diagnoseDropIn() {
  console.group('Checkout Drop-in diagnostics');
  
  // Check SDK installation
  console.log('SDK imported:', typeof CheckoutDropIn !== 'undefined');
  
  // Check container exists
  const container = document.getElementById('checkout-drop-in-container');
  console.log('Container exists:', !!container);
  console.log('Container visible:', container ? container.offsetParent !== null : false);
  
  // Check session data
  console.log('Session ID:', sessionData?.sessionId ? 'Present' : 'Missing');
  console.log('HMAC key:', sessionData?.hmacKey ? 'Present' : 'Missing');
  console.log('Allowed funding types:', sessionData?.allowedFundingTypes);
  
  // Check environment
  console.log('Environment:', config.environment);
  console.log('Owner ID:', config.ownerId);
  console.log('Protocol:', location.protocol);
  console.log('Hostname:', location.hostname);
  
  console.groupEnd();
}

Common issues

Drop-in not rendering

Symptoms:

  • There's an empty container where the drop-in should appear.
  • No payment methods are visible.
  • There are no errors in the console.
CauseSolution
Container ID mismatch.Verify that the container ID in your HTML matches the ID passed to create(). IDs are case-sensitive.
Container not in DOM.Ensure that the container element exists in the DOM before calling create(). Check the timing in React/Vue lifecycles. Use useEffect in React to ensure DOM is ready.
Session data invalid.Verify session data includes sessionId, hmacKey, and allowedFundingTypes. Check backend session creation.
No payment methods enabled.Check allowedFundingTypes in your session. At least one payment method must be enabled in the Unity Portal.
CSS conflicts.Check browser DevTools for CSS issues. Drop-in uses modern CSS that may conflict with old stylesheets.

Diagnostic steps:

async function diagnoseRenderingIssue() {
  // Step 1: Verify container
  const containerId = 'checkout-drop-in-container';
  const container = document.getElementById(containerId);
  
  if (!container) {
    console.error(`Container with ID "${containerId}" not found in DOM`);
    return;
  }
  console.log('Container found');
  
  // Step 2: Check container visibility
  const rect = container.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) {
    console.warn('Container has zero dimensions:', rect);
  } else {
    console.log('Container has dimensions:', rect);
  }
  
  // Step 3: Verify session data
  const sessionData = await fetch('/api/create-session').then(r => r.json());
  
  if (!sessionData.sessionId) {
    console.error('Session ID missing');
    return;
  }
  console.log('Session ID present:', sessionData.sessionId);
  
  if (!sessionData.allowedFundingTypes) {
    console.error('No allowed funding types in session');
    return;
  }
  console.log('Allowed funding types:', sessionData.allowedFundingTypes);
  
  // Step 4: Count available payment methods
  let paymentMethodCount = 0;
  if (sessionData.allowedFundingTypes.cards) paymentMethodCount++;
  if (sessionData.allowedFundingTypes.wallets?.paypal) paymentMethodCount++;
  if (sessionData.allowedFundingTypes.wallets?.googlePay) paymentMethodCount++;
  if (sessionData.allowedFundingTypes.wallets?.applePay) paymentMethodCount++;
  
  if (paymentMethodCount === 0) {
    console.error('No payment methods enabled in session');
    return;
  }
  console.log(`${paymentMethodCount} payment method(s) available`);
  
  // Step 5: Try initialisation
  try {
    const checkoutDropIn = CheckoutDropIn.initialize({
      environment: 'test',
      session: sessionData,
      ownerId: 'TEST',
      ownerType: 'MerchantGroup',
      transactionData: {
        currency: 'USD',
        amount: 1.00,
        entryType: 'Ecom',
        intent: {
          card: IntentType.Authorisation,
          paypal: IntentType.Purchase
        },
        merchantTransactionId: 'test-' + Date.now(),
        merchantTransactionDate: () => new Date().toISOString()
      },
      onGetShopper: () => Promise.resolve({ id: 'test-shopper' }),
      onSuccess: () => {},
      onError: () => {}
    });
    
    console.log('Drop-in initialised successfully');
    
    checkoutDropIn.create(containerId);
    console.log('Drop-in mounted successfully');
  } catch (error) {
    console.error('Initialisation failed:', error);
  }
}

Session expired errors

Symptoms:

  • A "Session expired" error message is displayed.
  • The drop-in loads but payment fails immediately.
  • The console shows session timeout errors.
CauseSolution
Session timeout exceeded.Sessions expire based on the sessionTimeout value (default = 120 minutes). Create a new session if expired.
Clock skewEnsure that the server and client clocks are synchronised. Use NTP for the server time.
Session reusedSessions are single-use. Create a new session for each checkout attempt.
Invalid HMAC signatureVerify that the HMAC key matches between session creation and SDK initialisation.

Solution: Implement session refresh

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

async function initializeWithSessionRefresh() {
  let sessionData = await getSessionFromBackend();
  
  const checkoutDropIn = CheckoutDropIn.initialize({
    environment: 'production',
    session: sessionData,
    ownerId: 'MERCHANT-1',
    ownerType: 'MerchantGroup',
    transactionData: {
      currency: 'USD',
      amount: 25.00,
      entryType: 'Ecom',
      intent: {
        card: IntentType.Authorisation,
        paypal: IntentType.Authorisation
      },
      merchantTransactionId: crypto.randomUUID(),
      merchantTransactionDate: () => new Date().toISOString()
    },
    onGetShopper: () => Promise.resolve({ id: 'shopper-001' }),
    onSuccess: (result) => handleSuccess(result),
    onError: async (error) => {
      // Handle session expiry
      if (error.code === 'SESSION_EXPIRED' || error.message.includes('expired')) {
        console.log('Session expired, refreshing...');
        
        try {
          // Destroy old instance first to prevent memory leaks
          checkoutDropIn.destroy();
          
          // Get new session
          sessionData = await getSessionFromBackend();
          
          // Reinitialise Drop-in
          const newDropIn = CheckoutDropIn.initialize({
            environment: 'production',
            session: sessionData,
            ownerId: 'MERCHANT-1',
            ownerType: 'MerchantGroup',
            transactionData: {
              currency: 'USD',
              amount: 25.00,
              entryType: 'Ecom',
              intent: {
                card: IntentType.Authorisation,
                paypal: IntentType.Authorisation
              },
              merchantTransactionId: crypto.randomUUID(),
              merchantTransactionDate: () => new Date().toISOString()
            },
            onGetShopper: () => Promise.resolve({ id: 'shopper-001' }),
            onSuccess: (result) => handleSuccess(result),
            onError: (error) => handleError(error)
          });
          
          newDropIn.create('checkout-drop-in-container');
          alert('Session refreshed. Please try your payment again.');
        } catch (refreshError) {
          console.error('Failed to refresh session:', refreshError);
          alert('Unable to refresh payment session. Please refresh the page.');
        }
      } else {
        handleError(error);
      }
    }
  });
  
  checkoutDropIn.create('checkout-drop-in-container');
}

async function getSessionFromBackend() {
  const response = await fetch('/api/create-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      amount: 25.00,
      currency: 'USD'
    })
  });
  
  if (!response.ok) {
    throw new Error('Failed to create session');
  }
  
  return response.json();
}

Payment method not appearing

Symptoms:

  • An expected payment method isn't shown.
  • Only the card payment method is visible.
  • The payment method briefly appears then disappears.
Payment methodCommon causesSolutions
Cards
  • Cards aren't enabled in the Unity Portal.
  • Missing `allowedFundingTypes.cards` in the session.
  • Enable the Card service in the Unity Portal.
  • Verify that the session includes the `cards` array
PayPal
  • PayPal onboarding wasn't completed.
  • Missing `allowedFundingTypes.wallets.paypal`.
  • Complete [PayPal onboarding].
  • Verify that the session includes PayPal configuration.
Google Pay
  • Unsupported browser or device.
  • Missing merchant ID in session.
  • Google Pay works on several browsers (Chrome, Edge, Opera, and more).
  • Configure Google Pay in the Unity Portal.
Apple Pay
  • Unsupported browser or device.
  • Unsupported device (no Touch ID/Face ID).
  • HTTPS required.
  • Apple Pay works on several browsers (Safari, Chrome, Edge, and more).
  • Configure Apple Pay in the Unity Portal.
  • Device must have biometric authentication.
  • Use HTTPS (or localhost for testing).

Diagnostic steps:

function diagnosePaymentMethodVisibility() {
  console.group('Payment method diagnostics');
  
  // Check session funding types
  const session = sessionData;
  const fundingTypes = session?.allowedFundingTypes;
  
  console.log('Session funding types:', fundingTypes);
  
  // Check cards
  if (fundingTypes?.cards) {
    console.log('Cards enabled:', fundingTypes.cards);
  } else {
    console.warn('Cards not enabled in session');
  }
  
  // Check PayPal
  if (fundingTypes?.wallets?.paypal) {
    console.log('PayPal enabled:', fundingTypes.wallets.paypal);
  } else {
    console.warn('PayPal not enabled in session');
  }
  
  // Check Google Pay
  if (fundingTypes?.wallets?.googlePay) {
    console.log('Google Pay configuration present');
    
    // Check browser compatibility
    const isChromiumBrowser = /Chrome|Edg|OPR/.test(navigator.userAgent);
    if (isChromiumBrowser) {
      console.log('Browser supports Google Pay');
    } else {
      console.warn('Browser does not support Google Pay (Chrome/Edge/Opera required)');
    }
  } else {
    console.warn('Google Pay not configured in session');
  }
  
  // Check Apple Pay
  if (fundingTypes?.wallets?.applePay) {
    console.log('Apple Pay configuration present');
    
    // Check browser compatibility
    const isSafari = /Safari/.test(navigator.userAgent) && !(/Chrome|Chromium|Edge|OPR/.test(navigator.userAgent));
    if (isSafari) {
      console.log('Browser is Safari');
      
      // Check Apple Pay availability
      if (window.ApplePaySession) {
        if (ApplePaySession.canMakePayments()) {
          console.log('Apple Pay available on this device');
        } else {
          console.warn('Apple Pay not set up on this device');
        }
      } else {
        console.warn('ApplePaySession not available');
      }
    } else {
      console.warn('Browser is not Safari (required for Apple Pay)');
    }
    
    // Check HTTPS
    if (location.protocol === 'https:' || location.hostname === 'localhost') {
      console.log('HTTPS requirement met');
    } else {
      console.warn('HTTPS required for Apple Pay');
    }
  } else {
    console.warn('Apple Pay not configured in session');
  }
  
  console.groupEnd();
}

PayPal popup blocked

Symptoms:

  • The PayPal window doesn't open.
  • A "Popup blocked" message is displayed in the browser.
  • The payment fails without any visible error.

Solution:

// Detect and handle popup blocker
function checkPopupBlocker() {
  // Try opening a test window
  const testWindow = window.open('', '_blank', 'width=1,height=1');
  
  if (!testWindow || testWindow.closed || typeof testWindow.closed === 'undefined') {
    // Popup blocked
    return true;
  }
  
  testWindow.close();
  return false;
}

// Show warning if popup blocker detected
if (checkPopupBlocker()) {
  console.warn('Popup blocker detected');
  
  // Show user-friendly message
  const warningDiv = document.createElement('div');
  warningDiv.className = 'popup-warning';
  warningDiv.innerHTML = `
    <div class="warning-content">
      <h4>Enable Popups</h4>
      <p>PayPal requires popup windows. Please allow popups for this site and try again.</p>
      <button onclick="this.parentElement.parentElement.remove()">Got it</button>
    </div>
  `;
  
  document.getElementById('checkout-drop-in-container').prepend(warningDiv);
}

Backend verification failing

Symptoms:

  • Frontend onSuccess fires but backend verification fails.
  • Orders aren't being fulfilled.
  • There are "Payment verification failed" errors.
CauseSolution
Webhook not configured.Set up a webhook URL in the Unity Portal and implement a webhook handler on your backend.
Webhook authentication failing.Verify your webhook signature/authentication. Check your HMAC implementation.
Race condition (GET before webhook).Implement a fallback to the Query Transaction API if the webhook hasn't arrived yet.
Amount mismatch.Ensure that the amount in the verification request exactly matches the transaction amount.
Transaction ID mismatchVerify that the systemTransactionId and merchantTransactionId match database records.

Solution: Robust backend verification

// Backend webhook handler (Node.js/Express example)
app.post('/webhooks/pxp', async (req, res) => {
  try {
    const events = req.body;
    
    // Verify webhook authenticity using HMAC (implement based on Unity webhook auth)
    if (!verifyWebhookSignature(req)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Unauthorised' });
    }
    
    for (const event of events) {
      if (event.eventCategory === 'Transaction') {
        const txn = event.eventData;
        
        console.log('Processing transaction:', txn.systemTransactionId);
        
        // Idempotency check
        const existing = await db.transactions.findOne({
          systemTransactionId: txn.systemTransactionId
        });
        
        if (existing) {
          console.log('Transaction already processed, skipping');
          continue;
        }
        
        // Verify transaction state
        if (txn.state === 'Authorised' || txn.state === 'Captured') {
          // Find order by merchant transaction ID
          const order = await db.orders.findOne({
            transactionId: txn.merchantTransactionId
          });
          
          if (!order) {
            console.error('Order not found:', txn.merchantTransactionId);
            continue;
          }
          
          // Verify amount matches (use appropriate field name from webhook payload)
          const transactionAmount = txn.amounts?.transactionValue || txn.amount;
          if (Math.abs(transactionAmount - order.amount) > 0.01) {
            console.error('Amount mismatch:', {
              expected: order.amount,
              actual: transactionAmount
            });
            continue;
          }
          
          // Mark transaction as processed
          await db.transactions.create({
            systemTransactionId: txn.systemTransactionId,
            merchantTransactionId: txn.merchantTransactionId,
            amount: transactionAmount,
            state: txn.state,
            processedAt: new Date()
          });
          
          // Fulfill order
          await fulfillOrder(order.id, txn.systemTransactionId);
          
          console.log('Order fulfilled:', order.id);
        }
      }
    }
    
    // Always return success
    res.json({ state: 'Success' });
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Frontend verification endpoint (fallback)
app.post('/api/verify-payment', async (req, res) => {
  const { systemTransactionId, merchantTransactionId, amount } = req.body;
  
  try {
    // Check if webhook already processed this
    const existing = await db.transactions.findOne({ systemTransactionId });
    
    if (existing) {
      const order = await db.orders.findOne({
        transactionId: merchantTransactionId
      });
      
      return res.json({
        success: true,
        orderId: order.id,
        source: 'webhook'
      });
    }
    
    // Fallback: Query PXP API
    // NOTE: You must implement HMAC authentication for PXP API calls
    // See the Backend Implementation section in the integration guide for HMAC details
    const response = await fetch(
      `https://api-services.pxp.io/api/v1/transactions?systemTransactionId=${systemTransactionId}&fundingType=Card`,
      {
        headers: {
          'X-Client-Id': process.env.PXP_CLIENT_ID,
          'X-Request-Id': crypto.randomUUID(),
          'Authorization': createAuthHeader('/api/v1/transactions', '', process.env.PXP_TOKEN_ID, process.env.PXP_TOKEN_VALUE)
        }
      }
    );
    
    if (!response.ok) {
      throw new Error('PXP API request failed');
    }
    
    const transaction = await response.json();
    
    // Verify transaction
    if (transaction.state !== 'Authorised' && transaction.state !== 'Captured') {
      return res.json({ success: false, error: 'Transaction not authorised' });
    }
    
    // Use appropriate field name from API response
    const transactionAmount = transaction.amounts?.transactionValue || transaction.amount;
    if (Math.abs(transactionAmount - amount) > 0.01) {
      return res.json({ success: false, error: 'Amount mismatch' });
    }
    
    if (transaction.merchantTransactionId !== merchantTransactionId) {
      return res.json({ success: false, error: 'Transaction ID mismatch' });
    }
    
    // Find and fulfill order
    const order = await db.orders.findOne({ transactionId: merchantTransactionId });
    
    if (!order) {
      return res.json({ success: false, error: 'Order not found' });
    }
    
    // Record transaction
    const transactionAmount = transaction.amounts?.transactionValue || transaction.amount;
    await db.transactions.create({
      systemTransactionId,
      merchantTransactionId,
      amount: transactionAmount,
      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' });
  }
});

Framework-specific issues

React: Drop-in not mounting

Symptoms:

  • The drop-in doesn't appear in the React component.
  • There are "Container not found" errors.
  • There are mount/unmount timing issues.

Solution:

import { useEffect, useRef } from 'react';
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';

export default function CheckoutPage() {
  const dropInRef = useRef(null);
  const mountedRef = useRef(false);

  useEffect(() => {
    // Prevent double-mounting in React StrictMode
    if (mountedRef.current) return;
    mountedRef.current = true;

    async function initializeDropIn() {
      try {
        const sessionData = await fetch('/api/create-session')
          .then(r => r.json());

        const checkoutDropIn = CheckoutDropIn.initialize({
          environment: 'production',
          session: sessionData,
          ownerId: 'MERCHANT-1',
          ownerType: 'MerchantGroup',
          transactionData: {
            currency: 'USD',
            amount: 25.00,
            entryType: 'Ecom',
            intent: {
              card: IntentType.Authorisation,
              paypal: IntentType.Authorisation
            },
            merchantTransactionId: crypto.randomUUID(),
            merchantTransactionDate: () => new Date().toISOString()
          },
          onGetShopper: () => Promise.resolve({ id: 'shopper-001' }),
          onSuccess: (result) => handleSuccess(result),
          onError: (error) => handleError(error)
        });

        // Store reference for cleanup
        dropInRef.current = checkoutDropIn;

        // Mount Drop-in to DOM
        checkoutDropIn.create('checkout-drop-in-container');
      } catch (error) {
        console.error('Failed to initialise Drop-in:', error);
      }
    }

    initializeDropIn();

    // Cleanup on unmount - always call destroy() to prevent memory leaks
    return () => {
      if (dropInRef.current) {
        dropInRef.current.destroy();
      }
    };
  }, []); // Empty dependency array - run once

  return (
    <div className="checkout-page">
      <h1>Complete your purchase</h1>
      <div id="checkout-drop-in-container"></div>
    </div>
  );
}

Vue: Reactivity issues

Solution:

<template>
  <div class="checkout-page">
    <h1>Complete your purchase</h1>
    <div id="checkout-drop-in-container"></div>
  </div>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import CheckoutDropIn from '@pxpio/web-components-sdk/src/checkoutDropIn/CheckoutDropIn';
import IntentType from '@pxpio/web-components-sdk/src/basePxpCheckout/types/IntentType';

const checkoutDropIn = ref(null);

onMounted(async () => {
  try {
    const sessionData = await fetch('/api/create-session')
      .then(r => r.json());

    checkoutDropIn.value = CheckoutDropIn.initialize({
      environment: 'production',
      session: sessionData,
      ownerId: 'MERCHANT-1',
      ownerType: 'MerchantGroup',
      transactionData: {
        currency: 'USD',
        amount: 25.00,
        entryType: 'Ecom',
        intent: {
          card: IntentType.Authorisation,
          paypal: IntentType.Authorisation
        },
        merchantTransactionId: crypto.randomUUID(),
        merchantTransactionDate: () => new Date().toISOString()
      },
      onGetShopper: () => Promise.resolve({ id: 'shopper-001' }),
      onSuccess: (result) => handleSuccess(result),
      onError: (error) => handleError(error)
    });

    checkoutDropIn.value.create('checkout-drop-in-container');
  } catch (error) {
    console.error('Failed to initialise Drop-in:', error);
  }
});

onUnmounted(() => {
  if (checkoutDropIn.value) {
    checkoutDropIn.value.destroy();
  }
});

function handleSuccess(result) {
  console.log('Payment successful:', result);
}

function handleError(error) {
  console.error('Payment failed:', error);
}
</script>

Error codes reference

For a complete list of error codes and their meanings, see the Error handling guide.

Getting additional help

If you're still experiencing issues, try these troubleshooting steps.

Enable debug mode

Add detailed logging using the analytics event handler:

const checkoutDropIn = CheckoutDropIn.initialize({
  // ... your config
  analyticsEvent: (event) => {
    console.log('Drop-in analytics event:', event);
  }
});

Collect diagnostic information

When contacting support, include:

function collectDiagnosticInfo() {
  const info = {
    // Browser information
    userAgent: navigator.userAgent,
    platform: navigator.platform,
    language: navigator.language,
    
    // Screen information
    screenWidth: window.screen.width,
    screenHeight: window.screen.height,
    viewport: {
      width: window.innerWidth,
      height: window.innerHeight
    },
    
    // Environment
    protocol: location.protocol,
    hostname: location.hostname,
    url: location.href,
    
    // SDK information
    sdkVersion: '1.0.0', // Replace with actual version
    environment: 'production',
    
    // Session info (sanitised)
    sessionIdPresent: !!sessionData?.sessionId,
    allowedFundingTypes: sessionData?.allowedFundingTypes,
    
    // Timestamp
    timestamp: new Date().toISOString()
  };
  
  console.log('Diagnostic information:', JSON.stringify(info, null, 2));
  
  // Copy to clipboard for easy sharing
  navigator.clipboard?.writeText(JSON.stringify(info, null, 2));
  
  return info;
}

When contacting support, always include your merchant ID, environment (test/production), and any relevant error messages or console logs.