Learn how to diagnose and fix common issues with Checkout Drop-in.
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();
}Symptoms:
- There's an empty container where the drop-in should appear.
- No payment methods are visible.
- There are no errors in the console.
| Cause | Solution |
|---|---|
| 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);
}
}Symptoms:
- A
"Session expired"error message is displayed. - The drop-in loads but payment fails immediately.
- The console shows session timeout errors.
| Cause | Solution |
|---|---|
| Session timeout exceeded. | Sessions expire based on the sessionTimeout value (default = 120 minutes). Create a new session if expired. |
| Clock skew | Ensure that the server and client clocks are synchronised. Use NTP for the server time. |
| Session reused | Sessions are single-use. Create a new session for each checkout attempt. |
| Invalid HMAC signature | Verify 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();
}Symptoms:
- An expected payment method isn't shown.
- Only the card payment method is visible.
- The payment method briefly appears then disappears.
| Payment method | Common causes | Solutions |
|---|---|---|
| Cards |
|
|
| PayPal |
|
|
| Google Pay |
|
|
| Apple Pay |
|
|
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();
}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);
}Symptoms:
- Frontend
onSuccessfires but backend verification fails. - Orders aren't being fulfilled.
- There are
"Payment verification failed"errors.
| Cause | Solution |
|---|---|
| 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 mismatch | Verify 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' });
}
});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>
);
}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>For a complete list of error codes and their meanings, see the Error handling guide.
If you're still experiencing issues, try these troubleshooting steps.
Add detailed logging using the analytics event handler:
const checkoutDropIn = CheckoutDropIn.initialize({
// ... your config
analyticsEvent: (event) => {
console.log('Drop-in analytics event:', event);
}
});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.