Implement callbacks to customise your Drop-in payment flow.
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.
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.
This callback receives no parameters and must return a Promise<Shopper>.
| Property | Description |
|---|---|
idstring | Unique shopper identifier. Use customer ID for logged-in users or generate a unique ID for guests. |
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.
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.
| Parameter | Description |
|---|---|
beforeany | Payment selection details containing information about the selected payment method. |
trueorundefined: Continue with payment submission.false: Cancel payment submission.Promise<boolean>: Async validation (resolved value determines whether to proceed).
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
}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.
| Parameter | Description |
|---|---|
submitany | Payment submission details containing information about the payment being processed. |
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();
}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 | Description |
|---|---|
resultBaseSubmitResult | The payment processing result from PXP's backend. |
result.systemTransactionIdstring | Unity's system transaction identifier. Use for backend verification. |
result.merchantTransactionIdstring | undefined | Your unique transaction identifier (optional). |
result.paymentMethodPaymentMethod | 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.
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' });
}
});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.
| Parameter | Description |
|---|---|
errorBaseSdkException | The error object containing details about what went wrong. |
error.messagestring | Human-readable error message. |
error.codestring | 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';The following table shows common error scenarios and how to detect and respond to them:
| Scenario | Detection approach | User action |
|---|---|---|
| Card declined | error.message contains "declined" or specific provider messages | Try a different card. |
| Insufficient funds | error.message contains "insufficient funds" | Use a different payment method. |
| Expired card | error.message contains "expired" | Use a different card. |
| Invalid CVV | error.message contains "CVV" or "security code" | Check security code. |
| 3DS authentication failed | error.code === 'SDK1114' or error.message contains "Authentication failed" | Try again or use different card. |
| 3DS timeout | error.message contains "timeout" | Check connection and retry. |
| User cancelled 3DS | error.message contains "cancel" | Retry payment. |
| PayPal error | error.code === 'SDK1117' or payment method is PayPal | Try again or use different method. |
| Session expired | error.message contains "session" or "expired" | Refresh page and retry. |
| Network error | error.code === 'SDK0500' | Check connection and retry. |
| Configuration error | error.code starts with 'SDK01' or 'SDK02' | Contact support. |
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();
}
}
}
});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';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';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';The onGetShopper callback must return a Shopper object:
interface Shopper {
id: string; // Unique shopper identifier
}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()
}
});
}
});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:
- Configuration guide - Payment method-specific configuration.
- PayPal documentation - PayPal callbacks and settings.
- Apple Pay documentation - Apple Pay callbacks and settings.
- Google Pay documentation - Google Pay callbacks and settings.
Understanding the callback flow helps you implement proper payment handling:
onGetShopper - Initial setup
- Called during Drop-in initialisation.
- Must return shopper ID for card-on-file functionality.
onBeforeSubmit - Validation phase
- User selects payment method and clicks submit.
- Receives payment selection details as parameter.
- Return
trueto proceed,falseto cancel. - Perform business validation and fraud checks.
onSubmit - Processing starts (if onBeforeSubmit returns true or undefined)
- Receives payment submission details as parameter.
- Show loading indicators.
- Disable form fields.
- Track analytics.
onSuccess OR onError - Payment completes
- onSuccess - Payment succeeded (verify on backend before fulfilling).
- onError - Payment failed (show error message and offer alternatives).
onCancel - User cancels (optional)
- Triggered if user closes payment window/sheet.
- Update UI and track analytics.
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 UIHere'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();