Confirm payment flow
Authorise a payment, then capture it before it expires.
Overview
The confirm payment flow is designed for scenarios where you need to authorise payment first, then capture it after additional confirmation steps, making it ideal for complex orders, inventory checks, or when manual review is required.
If the funds aren't captured within the appropriate time limit (29 days for PayPal or 7 days for Venmo), then the authorisation expires.
Payment flow
The PayPal confirm payment flow consists of six key steps with authorisation and capture separation.
Step 1: Submission
The customer clicks the PayPal button, which triggers the payment flow. The SDK validates the PayPal configuration and transaction data before proceeding to order creation. If validation fails, onError
is triggered.
Step 2: Order creation
The SDK creates a PayPal order using the provided transaction details. This involves sending the payment amount, currency, merchant information, and any additional order details to PayPal's API for authorisation. If order creation fails, onError
is triggered.
Step 3: PayPal approval
The customer is redirected to PayPal where they log in and approve the payment authorisation. PayPal handles all authentication and payment method selection within their secure environment.
This step has three associated callbacks:
onApprove
: Proceeds with payment authorisation if the customer successfully approves it.onCancel
: Cancels the transaction if the customer cancels the payment.onError
: Receives error data if any error occurs during the approval process.
Step 4: Return to merchant
After PayPal approval, the customer returns to your site with the authorised payment. The funds are authorised but not yet captured, giving you the opportunity to show order confirmation. This happens within your onApprove
callback handler where you typically redirect to a confirmation page.
Step 5: Order confirmation
The customer reviews their final order details on your confirmation page. This is where you can perform final inventory checks, calculate shipping, or apply additional discounts.
Step 6: Payment capture
Once the customer confirms their order, you capture the authorised payment. This is when the funds are actually transferred from the customer's account.
Your capture API call will return a success or failure response, which you handle in your backend code.
Implementation
Before you start
To use the PayPal confirm payment flow in your application:
- Ensure you have a valid PayPal Business account with API credentials.
- Configure your PayPal merchant account to accept the currencies you need.
- Set up your merchant configuration in the Unity Portal (API credentials, payment methods, risk settings).
- Prepare order confirmation pages for the authorisation-to-capture flow.
Step 1: Configure your SDK
Set up your sdkConfig
with transaction information for a PayPal authorisation.
const sdkConfig = {
transactionData: {
amount: 2500,
currency: 'USD',
entryType: 'Ecom',
intent: 'Create',
merchantTransactionId: 'order-' + Date.now(),
merchantTransactionDate: () => new Date().toISOString(),
// Optional shopper data
shopper: {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe'
}
},
// Your other SDK configuration
};
Step 2: Implement callbacks
Implement the required callbacks for the PayPal confirm payment flow.
const paypalComponent = pxpSdk.create('paypal-button', {
// Required PayPal configuration
payeeEmailAddress: '[email protected]',
paymentDescription: 'Product Purchase',
shippingPreference: 'NO_SHIPPING',
userAction: 'CONTINUE', // For authorisation + confirmation flow
renderType: 'standalone',
fundingSources: 'paypal',
// REQUIRED: Handle successful payment authorisation
onApprove: async (data, actions) => {
console.log('PayPal payment authorised:', data);
try {
// Store authorisation details
const authResult = await fetch('/api/store-paypal-authorization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderID: data.orderID,
payerID: data.payerID,
merchantTransactionId: sdkConfig.transactionData.merchantTransactionId,
status: 'authorized'
})
});
const response = await authResult.json();
if (response.success) {
console.log('Authorisation stored successfully');
// Redirect to confirmation page instead of completing payment
window.location.href = `/order-confirmation?orderID=${data.orderID}`;
} else {
throw new Error(response.error || 'Authorisation storage failed');
}
} catch (error) {
console.error('Authorisation processing error:', error);
showError('Authorisation failed. Please try again.');
}
},
// REQUIRED: Handle payment errors
onError: (error) => {
console.error('PayPal payment error:', error);
showError('Payment failed. Please try again.');
},
// OPTIONAL: Handle payment cancellation
onCancel: (data) => {
console.log('PayPal payment cancelled:', data);
showMessage('Payment was cancelled. You can try again anytime.');
}
});
Step 3: Handle order confirmation page
Create a separate flow for handling the order confirmation and proceeding with the final capture.
// On your order confirmation page
async function confirmAndCaptureOrder(orderID) {
try {
showLoadingSpinner();
// Retrieve the stored authorisation data
const authData = JSON.parse(sessionStorage.getItem('paypalAuth'));
if (!authData) {
throw new Error('Authorisation data not found');
}
// Perform final checks (inventory, pricing, etc.)
const orderValid = await validateOrder(orderID);
if (!orderValid) {
throw new Error('Order validation failed');
}
// Calculate the final amount (may include shipping, taxes, discounts)
const finalAmount = calculateFinalAmount();
// Create new SDK configuration for capture
const captureSDKConfig = {
environment: "test", // or "live"
session: sessionData, // Same session data as the initial request
ownerId: "your-owner-id",
ownerType: "MerchantGroup",
merchantShopperId: "shopper-123",
transactionData: {
amount: finalAmount, // Final amount after any adjustments
currency: 'USD',
entryType: 'Ecom',
intent: 'Capture',
merchantTransactionId: 'capture-' + Date.now(), // New transaction ID
merchantTransactionDate: () => new Date().toISOString(),
// Reference to original authorization
parentTransactionId: authData.merchantTransactionId
}
};
// Initialise a new SDK instance for capture
const capturePxpSdk = PxpCheckout.initialize(captureSDKConfig);
// Create a PayPal capture component
const captureResult = await capturePxpSdk.capturePayPalAuthorization({
authorizationOrderID: authData.orderID,
payerID: authData.payerID,
captureAmount: finalAmount,
onCaptureSuccess: (result) => {
console.log('Payment captured successfully:', result);
// Store capture details
sessionStorage.setItem('paymentCapture', JSON.stringify({
captureID: result.captureID,
transactionId: result.transactionId,
amount: result.amount,
status: 'completed'
}));
// Redirect to the success page
window.location.href = `/payment-success?orderID=${orderID}&captureID=${result.captureID}`;
},
onCaptureError: (error) => {
console.error('Capture failed:', error);
handleCaptureError(error);
}
});
} catch (error) {
console.error('Order confirmation error:', error);
hideLoadingSpinner();
showError('Order confirmation failed: ' + error.message);
}
}
// Handle capture-specific errors
function handleCaptureError(error) {
hideLoadingSpinner();
if (error.code === 'AUTHORIZATION_EXPIRED') {
showError('Payment authorisation has expired. Please start the payment process again.');
setTimeout(() => window.location.href = '/checkout', 3000);
} else if (error.code === 'INSUFFICIENT_FUNDS') {
showError('Insufficient funds for final payment amount. Please try a different payment method.');
} else if (error.code === 'CAPTURE_DECLINED') {
showError('Payment capture was declined. Please contact your bank or try a different payment method.');
} else if (error.code === 'INVALID_AUTHORIZATION') {
showError('Authorisation is no longer valid. Please restart the payment process.');
} else {
showError('Payment capture failed. Please contact support or try again.');
}
}
Step 4: Handle common scenarios
Inventory validation
Validate inventory between authorisation and capture.
const paypalComponent = pxpSdk.create('paypal-button', {
onApprove: async (data, actions) => {
try {
// Store authorisation and check inventory
const inventoryCheck = await fetch('/api/reserve-inventory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderID: data.orderID,
items: getCartItems(),
reservationDuration: 3600 // 1 hour reservation
})
});
const inventoryResult = await inventoryCheck.json();
if (inventoryResult.success) {
// Redirect to confirmation with inventory reserved
window.location.href = `/order-confirmation?orderID=${data.orderID}&reservation=${inventoryResult.reservationId}`;
} else {
throw new Error('Inventory not available');
}
} catch (error) {
console.error('Inventory check failed:', error);
showError('Some items are no longer available. Please review your cart.');
}
}
});
Shipping calculation
Calculate final shipping costs during confirmation.
// On confirmation page
async function calculateShippingAndConfirm(orderID, shippingAddress) {
try {
const shippingResult = await fetch('/api/calculate-final-shipping', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderID: orderID,
shippingAddress: shippingAddress,
items: getOrderItems()
})
});
const shipping = await shippingResult.json();
if (shipping.success) {
// Show final total including shipping
updateOrderSummary({
subtotal: shipping.subtotal,
shippingCost: shipping.shippingCost,
total: shipping.total
});
// Enable confirm button with final amount
enableConfirmButton(shipping.total);
} else {
throw new Error('Shipping calculation failed');
}
} catch (error) {
console.error('Shipping calculation error:', error);
showError('Unable to calculate shipping. Please try again.');
}
}
Step 5: Handle errors
Implement comprehensive error handling for both the authorisation and capture phases.
const paypalComponent = pxpSdk.create('paypal-button', {
onError: (error) => {
console.error('PayPal error:', error);
// Handle specific PayPal error types
if (error.name === 'VALIDATION_ERROR') {
showError('Payment details validation failed. Please try again.');
} else if (error.name === 'INSTRUMENT_DECLINED') {
showError('Your PayPal payment method was declined. Please try a different method.');
} else if (error.name === 'PAYER_ACTION_REQUIRED') {
showError('Additional action required in PayPal. Please complete the process.');
} else if (error.name === 'UNPROCESSABLE_ENTITY') {
showError('Payment cannot be processed. Please contact support.');
} else {
showError('Payment failed. Please try again or contact support.');
}
},
onApprove: async (data, actions) => {
try {
await processPayPalAuthorization(data);
} catch (error) {
// Handle authorisation errors
if (error.status === 422) {
showError('Payment authorization failed. Please try again.');
} else if (error.status === 409) {
showError('This payment has already been processed.');
} else if (error.status >= 500) {
showError('Payment system temporarily unavailable. Please try again later.');
} else {
showError('Authorization failed. Please try again.');
}
}
}
});
// Handle capture errors on confirmation page
async function handleCaptureErrors(error) {
if (error.code === 'AUTHORIZATION_EXPIRED') {
showError('Payment authorization has expired. Please start over.');
// Redirect back to checkout
setTimeout(() => window.location.href = '/checkout', 3000);
} else if (error.code === 'INSUFFICIENT_FUNDS') {
showError('Insufficient funds for payment. Please try a different payment method.');
} else if (error.code === 'CAPTURE_DECLINED') {
showError('Payment was declined during processing. Please contact your bank.');
} else {
showError('Payment capture failed. Please contact support.');
}
}
Example
The following example shows a complete PayPal confirm payment implementation with two separate intents.
Phase 1: Authorisation (Checkout Page)
// FIRST INTENT: Authorisation
const authSDKConfig = {
transactionData: {
amount: 2500,
currency: 'USD',
entryType: 'Ecom',
intent: 'Create', // FIRST INTENT - for authorisation
merchantTransactionId: 'auth-' + Date.now(),
merchantTransactionDate: () => new Date().toISOString(),
}
};
const authPxpSdk = PxpCheckout.initialize(authSDKConfig);
const paypalComponent = authPxpSdk.create('paypal-button', {
// PayPal configuration for authorization flow
payeeEmailAddress: '[email protected]',
paymentDescription: 'Product Purchase - Review Order',
shippingPreference: 'NO_SHIPPING',
userAction: 'CONTINUE', // Authorization + confirmation flow
renderType: 'standalone',
fundingSources: 'paypal',
// Styling
style: {
layout: 'vertical',
color: 'gold',
shape: 'rect',
label: 'paypal'
},
// Step 1: Handle payment authorization
onApprove: async (data, actions) => {
console.log('Processing PayPal authorization');
showLoadingSpinner();
try {
console.log(`Order ID: ${data.orderID}`);
console.log(`Authorized Amount: $${(authSDKConfig.transactionData.amount / 100).toFixed(2)}`);
// Store authorisation data for capture step
sessionStorage.setItem('paypalAuth', JSON.stringify({
orderID: data.orderID,
payerID: data.payerID,
merchantTransactionId: authSDKConfig.transactionData.merchantTransactionId,
authorizedAmount: authSDKConfig.transactionData.amount,
currency: authSDKConfig.transactionData.currency
}));
// Redirect to confirmation page
window.location.href = `/order-confirmation?orderID=${data.orderID}`;
} catch (error) {
console.error('PayPal authorisation failed:', error);
hideLoadingSpinner();
showError('Authorisation failed: ' + (error.message || 'Please try again'));
}
},
onCancel: (data) => {
console.log('PayPal authorisation cancelled by user');
showMessage('Payment was cancelled. Your cart is still saved.');
},
onError: (error) => {
console.error('PayPal authorisation error:', error);
hideLoadingSpinner();
showError('Authorisation failed. Please try again.');
}
});
// Mount the authorisation component
paypalComponent.mount('paypal-button-container');
Phase 2: Capture (Confirmation page)
// SECOND INTENT: Capture
async function captureAuthorizedPayment() {
try {
// Retrieve authorisation data
const authData = JSON.parse(sessionStorage.getItem('paypalAuth'));
if (!authData) throw new Error('Authorisation data not found');
// Calculate final amount (may be different due to shipping, taxes, etc.)
const finalAmount = calculateFinalAmount(); // e.g., 2750 ($27.50 with shipping)
// Create NEW SDK configuration for capture
const captureSDKConfig = {
transactionData: {
amount: finalAmount, // Final amount
currency: authData.currency,
entryType: 'Ecom',
intent: 'Capture', // SECOND INTENT - for capture
merchantTransactionId: 'capture-' + Date.now(), // New transaction ID
merchantTransactionDate: () => new Date().toISOString(),
parentTransactionId: authData.merchantTransactionId // Link to authorization
}
};
// Initialise new SDK instance for capture
const capturePxpSdk = PxpCheckout.initialize(captureSDKConfig);
// Execute the capture
const captureResult = await capturePxpSdk.capturePayPalAuthorization({
authorizationOrderID: authData.orderID,
payerID: authData.payerID,
captureAmount: finalAmount,
onCaptureSuccess: (result) => {
console.log('Payment captured successfully:', result);
console.log(`Captured Amount: $${(result.amount / 100).toFixed(2)}`);
// Store capture confirmation
sessionStorage.setItem('paymentCapture', JSON.stringify({
captureID: result.captureID,
transactionId: result.transactionId,
amount: result.amount,
status: 'completed'
}));
// Redirect to success page
window.location.href = `/payment-success?captureID=${result.captureID}`;
},
onCaptureError: (error) => {
console.error('Capture failed:', error);
if (error.code === 'AUTHORIZATION_EXPIRED') {
showError('Authorisation expired. Please restart payment.');
setTimeout(() => window.location.href = '/checkout', 3000);
} else {
showError('Payment capture failed. Please contact support.');
}
}
});
} catch (error) {
console.error('Capture process failed:', error);
showError('Unable to complete payment. Please try again.');
}
}
// Call this function when customer confirms their order
document.getElementById('confirm-order-btn').addEventListener('click', captureAuthorizedPayment);
Callback data
This section describes the data received by the different callbacks as part of the PayPal confirm payment flow.
onApprove
The onApprove
callback receives authorization approval data when the customer successfully authorizes the payment in PayPal.
Authorisation approval data
The approval data includes the PayPal order ID and payer information needed for later capture.
{
orderID: "7YH53119ML8957234",
payerID: "ABCDEFGHIJKLM",
paymentID: "PAYID-ABCDEFG",
billingToken: null,
facilitatorAccessToken: "A21AAFExi..."
}
Parameter | Description |
---|---|
orderID string required | The unique PayPal order ID that identifies this authorisation. |
payerID string required | The PayPal payer ID that identifies the customer who authorised the payment. |
paymentID string | The PayPal payment ID for this authorisation. |
billingToken string or null | The billing agreement token if applicable, otherwise null . |
facilitatorAccessToken string | The PayPal facilitator access token for processing the authorisation. |
Here's an example of what to do with this data:
onApprove: async (data, actions) => {
console.log('PayPal payment authorised:', data);
try {
// Store the authorisation for later capture
const authResponse = await fetch('/api/paypal/store-authorization', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderID: data.orderID,
payerID: data.payerID,
// Add merchant context
merchantTransactionId: generateTransactionId(),
timestamp: new Date().toISOString(),
// Add session info for confirmation page
sessionData: {
cartItems: getCartItems(),
shippingAddress: getShippingAddress(),
customerEmail: getCustomerEmail()
}
})
});
const result = await authResponse.json();
if (result.success) {
// Authorisation stored successfully
console.log('Authorisation stored:', result.authorizationId);
// Store in session for confirmation page
sessionStorage.setItem('paypalAuth', JSON.stringify({
orderID: data.orderID,
authorizationId: result.authorizationId,
payerID: data.payerID,
authorizedAmount: result.authorizedAmount,
expiresAt: result.expiresAt // Authorisation expiry time
}));
// Redirect to confirmation page
window.location.href = `/order-confirmation?orderID=${data.orderID}`;
} else {
throw new Error(`Authorization storage failed: ${result.error}`);
}
} catch (error) {
console.error('Authorisation processing error:', error);
showError('Authorisation failed. Please try again.');
}
}
onError
The onError
callback receives error information when PayPal authorisation fails.
Error data
PayPal errors include specific error names and details to help with troubleshooting.
{
name: "VALIDATION_ERROR",
message: "Invalid payment method",
details: [{
issue: "INSTRUMENT_DECLINED",
description: "The instrument presented was either declined by the processor or bank, or it can't be used for this payment."
}]
}
Parameter | Description |
---|---|
| The error name.
|
| A human-readable error message. |
| An array of detailed error information. |
| The specific issue code from PayPal. |
| A detailed description of the issue. |
Here's an example of how to handle PayPal authorisation errors:
onError: (error) => {
console.error('PayPal authorisation error:', error);
// Handle specific error types
switch (error.name) {
case 'VALIDATION_ERROR':
showError('Payment information is invalid. Please try again.');
logError('PayPal validation error', error);
break;
case 'INSTRUMENT_DECLINED':
showError('Your PayPal payment method was declined. Please try a different payment method.');
break;
case 'PAYER_ACTION_REQUIRED':
showError('Additional verification required. Please complete the process in PayPal.');
break;
case 'UNPROCESSABLE_ENTITY':
showError('Payment cannot be authorized. Please contact support.');
logError('PayPal unprocessable entity', error);
break;
case 'INTERNAL_SERVICE_ERROR':
showError('PayPal service temporarily unavailable. Please try again later.');
break;
default:
showError('Authorisation failed. Please try again or contact support.');
logError('Unknown PayPal error', error);
}
// Log error details for monitoring
logPaymentError({
errorName: error.name,
errorMessage: error.message,
errorDetails: error.details,
timestamp: new Date().toISOString(),
paymentMethod: 'paypal',
flow: 'confirm'
});
}
onCancel
The onCancel
callback receives data when the customer cancels the PayPal authorisation process.
Cancellation data
The cancellation data includes the order ID and reason for the cancellation.
{
orderID: "7YH53119ML8957234",
reason: "user_cancelled"
}
Parameter | Description |
---|---|
orderId string required | The PayPal order ID that was being processed when the cancellation happened. |
reason string | The reason for cancellation. Typically, this is user_cancelled . |
Here's an example of how to handle authorisation cancellations:
onCancel: (data) => {
console.log('PayPal authorisation cancelled:', data);
// Log cancellation for analytics
logPaymentCancellation({
orderID: data.orderID,
reason: data.reason || 'user_cancelled',
timestamp: new Date().toISOString(),
paymentMethod: 'paypal',
flow: 'confirm'
});
// Show user-friendly message
showMessage('Payment authorisation was cancelled. Your cart items are still saved.');
// Optional: Offer alternative payment methods or restart process
showAlternativePaymentOptions();
}
Updated 13 days ago