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:

  1. Ensure you have a valid PayPal Business account with API credentials.
  2. Configure your PayPal merchant account to accept the currencies you need.
  3. Set up your merchant configuration in the Unity Portal (API credentials, payment methods, risk settings).
  4. 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..."
}
ParameterDescription
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

name
string

The error name.

Possible values:

  • VALIDATION_ERROR
  • INSTRUMENT_DECLINED
  • PAYER_ACTION_REQUIRED
  • UNPROCESSABLE_ENTITY

message
string

A human-readable error message.

details
array of string

An array of detailed error information.

details[].issue
string

The specific issue code from PayPal.

details[].description
string

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"
}
ParameterDescription
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();
}