Skip to content

Withdrawal flow

Send payouts to returning customers using stored wallet details.

Overview

The withdrawal flow is designed for returning customers whose wallet details you already have stored. This provides a faster, streamlined experience — the customer simply reviews the payout details and confirms, without needing to log in again.

This flow uses the receiver and submission components together, displaying the stored wallet information and providing a "Withdraw" button.

Payout flow

The withdrawal flow consists of five key steps for returning customer payouts.

Step 1: Display payout details

The customer sees their stored PayPal email or Venmo handle displayed alongside the payout amount. The receiver component shows the wallet destination, optionally masked for privacy.

Step 2: Submission

The customer clicks the "Withdraw with PayPal" or "Withdraw with Venmo" button. The SDK validates the payout configuration and stored wallet details before proceeding. If validation fails, onError is triggered.

Step 3: Payout approval

The onPrePayoutSubmit callback is triggered, giving you the opportunity to show a confirmation dialog or perform additional validation before the payout executes.

Return { isApproved: true } to proceed with the payout, or { isApproved: false } to cancel.

Step 4: Payout execution

For the SDK-initiated mode, the SDK automatically sends the payout request to the PXP Gateway. For merchant-initiated mode, your backend triggers the payout via API.

Step 5: Payout result

The onPostPayout callback receives the transaction result. You can display a success message and redirect the customer to a confirmation page.

Implementation

Before you start

To use the withdrawal flow for payouts:

  1. Ensure your PayPal merchant account is onboarded with PXP.
  2. Have sufficient funds in your gateway balance to cover payout amounts and fees.

Step 1: Configure your SDK

Set up your SDK configuration with wallet details to trigger the withdrawal flow.

import { PxpCheckout, IntentType, CurrencyType } from "@pxpio/web-components-sdk";

const pxpCheckoutSdk = PxpCheckout.initialize({
  environment: "test", // or "production" for live
  session: sessionData,
  ownerId: "your-owner-id",
  ownerType: "MerchantGroup",
  transactionData: {
    amount: 100,
    currency: "USD" as CurrencyType,
    entryType: "Ecom",
    intent: {
      paypal: IntentType.Payout
    },
    merchantTransactionId: crypto.randomUUID(),
    merchantTransactionDate: () => new Date().toISOString()
  },
  paypalConfig: {
    payoutConfig: {
      proceedPayoutWithSdk: true,
      paypalWallet: {
        email: "customer@example.com",  // Stored PayPal email
        payerId: "PAYER_ID_XXX"         // Required: stored payer ID
      }
    }
  }
});

Providing paypalWallet or venmoWallet properties signals to the SDK that this is a returning customer with stored wallet details.

Step 2: Add the HTML containers

Add HTML containers for each component.

<div class="payout-page">
  <h1>Withdraw funds</h1>
  <div id="payout-amount-container"></div>
  <div id="payout-receiver-container"></div>
  <div id="payout-submit-container"></div>
</div>

Step 3: Create and mount the components

Use the amount, receiver, and submission components to build the withdrawal experience.

// Create the amount display component
const amountComponent = pxpCheckoutSdk.create('payout-amount', {
  label: "Withdrawal Amount"
});

// Create the PayPal receiver display component
const receiverComponent = pxpCheckoutSdk.create('paypal-payout-receiver', {
  label: "PayPal Account",
  showMaskToggle: true,  // Display a toggle (eye icon) to allow the customer to show or hide their full email
  applyMask: true        // Start with masked email
});

// Create the submission button for PayPal
const submitComponent = pxpCheckoutSdk.create('payout-submission', {
  recipientWallet: "Paypal",
  
  // OPTIONAL: Called before payout execution
  onPrePayoutSubmit: async () => {
    const confirmed = await showConfirmationDialog();
    return { 
      isApproved: confirmed,
      note: "Customer withdrawal"  // Optional note (max 4000 chars)
    };
  },
  
  // OPTIONAL: Called when the payout completes successfully
  onPostPayout: (result) => {
    console.log('Payout successful:', result.transactionId);
    showSuccessMessage('Your withdrawal has been processed!');
    window.location.href = `/success?txn=${result.transactionId}`;
  },
  
  // OPTIONAL: Called on any error
  onError: (error) => {
    console.error('Payout failed:', error);
    showErrorMessage('Withdrawal failed. Please try again.');
  }
});

// Mount all components
amountComponent.mount('payout-amount-container');
receiverComponent.mount('payout-receiver-container');
submitComponent.mount('payout-submit-container');

Step 4: Handle payout modes

When proceedPayoutWithSdk: true, the SDK handles the complete flow:

  1. The customer sees their stored wallet details.
  2. The customer clicks "Withdraw with PayPal/Venmo".
  3. onPrePayoutSubmit is called for approval.
  4. The SDK executes the payout automatically.
  5. onPostPayout is called with the result.
paypalConfig: {
  payoutConfig: {
    proceedPayoutWithSdk: true,  // SDK handles payout execution
    paypalWallet: {
      email: "customer@example.com"
    }
  }
}

Step 5: Handle errors

Implement comprehensive error handling for the payout process.

const submitComponent = pxpCheckoutSdk.create('payout-submission', {
  recipientWallet: "Paypal",
  
  onError: (error) => {
    console.error('Payout error:', error);
    
    // Handle specific error types
    switch (error.ErrorCode) {
      case 'SDK0809':
        showError('PayPal account information is missing.');
        break;
      case 'SDK0810':
      case 'SDK0811':
      case 'SDK0812':
        showError('Invalid payout amount. Please contact support.');
        break;
      case 'SDK0813':
      case 'SDK0814':
      case 'SDK0815':
        showError('Invalid currency.');
        break;
      case 'SDK1001':
        showError('Recipient wallet is required.');
        break;
      case 'SDK1003':
        showError('Unsupported wallet type.');
        break;
      case 'SDK1004':
        showError('Payout note is too long (max 4000 characters).');
        break;
      default:
        showError('An error occurred. Please try again or contact support.');
    }
  }
});

Step 6: Unmount the components

Clean up the components when they're no longer needed.

amountComponent.unmount();
receiverComponent.unmount();
submitComponent.unmount();

Example

The following example shows a complete withdrawal flow implementation for returning customers.

import { PxpCheckout, IntentType, CurrencyType } from "@pxpio/web-components-sdk";

async function initializeWithdrawalPage() {
  // Get session data from your backend
  const sessionData = await getSessionDataFromBackend();
  
  // Get stored customer wallet details
  const customerWallet = await getStoredCustomerWallet();
  const payoutAmount = 150.00;
  
  // Initialize SDK with stored wallet details (withdrawal flow)
  const pxpCheckoutSdk = PxpCheckout.initialize({
    environment: "test",
    session: sessionData,
    ownerId: "Unity",
    ownerType: "MerchantGroup",
    transactionData: {
      amount: payoutAmount,
      currency: "USD" as CurrencyType,
      entryType: "Ecom",
      intent: { paypal: IntentType.Payout },
      merchantTransactionId: crypto.randomUUID(),
      merchantTransactionDate: () => new Date().toISOString()
    },
    paypalConfig: {
      payoutConfig: {
        proceedPayoutWithSdk: true,
        paypalWallet: {
          email: customerWallet.email,
          payerId: customerWallet.payerId
        }
      }
    }
  });

  // Create amount display component
  const amountComponent = pxpCheckoutSdk.create('payout-amount', {
    label: "Withdrawal Amount",
    style: {
      fontSize: "24px",
      fontWeight: "bold",
      color: "#2e7d32"
    }
  });

  // Create receiver display component
  const receiverComponent = pxpCheckoutSdk.create('paypal-payout-receiver', {
    label: "Sending to",
    showMaskToggle: true,
    applyMask: true,
    style: {
      backgroundColor: "#f5f5f5",
      padding: "16px",
      borderRadius: "8px"
    }
  });

  // Create submission button
  const submitComponent = pxpCheckoutSdk.create('payout-submission', {
    recipientWallet: "Paypal",
    submitText: "Withdraw to PayPal",
    
    styles: {
      base: {
        backgroundColor: "#FFC438",
        color: "#09090C",
        borderRadius: "8px",
        padding: "16px 32px",
        fontSize: "18px",
        fontWeight: "bold",
        width: "100%"
      },
      hover: {
        backgroundColor: "#F7B731"
      }
    },
    
    onClick: () => {
      trackEvent("payout_button_clicked");
    },
    
    onPrePayoutSubmit: async () => {
      // Perform validation
      const balance = await checkUserBalance();
      if (balance < payoutAmount) {
        showError("Insufficient balance for this withdrawal.");
        return { isApproved: false };
      }
      
      // Show confirmation dialog
      const confirmed = await showConfirmationModal({
        title: "Confirm Withdrawal",
        details: [
          `Amount: $${payoutAmount.toFixed(2)}`,
          `To: ${maskEmail(customerWallet.email)}`,
          `Processing time: 1-2 business days`
        ],
        confirmText: "Confirm Withdrawal",
        cancelText: "Cancel"
      });
      
      if (!confirmed) {
        trackEvent("payout_cancelled_by_user");
        return { isApproved: false };
      }
      
      trackEvent("payout_approved");
      return { 
        isApproved: true,
        note: `Withdrawal request from user ${getCurrentUserId()}`
      };
    },
    
    onPostPayout: (data) => {
      console.log("Payout successful:", data);
      
      trackEvent("payout_completed", {
        merchantTransactionId: data.merchantTransactionId,
        systemTransactionId: data.systemTransactionId,
        amount: payoutAmount
      });
      
      // Update local balance
      updateUserBalance(balance - payoutAmount);
      
      // Show success animation
      showSuccessAnimation();
      
      // Redirect to success page
      setTimeout(() => {
        window.location.href = `/success?txn=${data.merchantTransactionId}`;
      }, 1500);
    },
    
    onError: (error) => {
      console.error("Payout error:", error);
      
      trackEvent("payout_failed", {
        errorCode: error.errorCode,
        httpStatusCode: error.httpStatusCode
      });
      
      handlePayoutError(error);
    }
  });

  // Mount all components
  amountComponent.mount('payout-amount-container');
  receiverComponent.mount('payout-receiver-container');
  submitComponent.mount('payout-submit-container');
}

initializeWithdrawalPage();
<div class="payout-page returning-customer">
  <h1>Withdraw funds</h1>
  <p class="subtitle">Review your withdrawal details below</p>
  
  <div class="payout-card">
    <div id="payout-amount-container"></div>
    <div class="divider"></div>
    <div id="payout-receiver-container"></div>
    <div class="divider"></div>
    <div id="payout-submit-container"></div>
  </div>
  
  <p class="info-text">
    <a href="/account/change-payout-method">Use a different PayPal account?</a>
  </p>
</div>

Callback data

This section describes the data received by the different callbacks as part of the withdrawal flow.

onPrePayoutSubmit

The onPrePayoutSubmit callback is called before payout execution. Return an object indicating whether to proceed.

onPrePayoutSubmit: async () => {
  const approved = await showConfirmationDialog();
  return { 
    isApproved: approved,
    note: "Optional transaction note"  // Max 4000 characters
  };
}
Return PropertyDescription
isApproved
boolean
required
Whether to proceed with the payout. Return false to cancel.
note
string
Optional note to include with the payout transaction (max 4000 characters).

onPostPayout

The onPostPayout callback receives the payout result when the transaction completes successfully.

onPostPayout: (data) => {
  console.log("Transaction details:", data.merchantTransactionId, data.systemTransactionId);
}
ParameterDescription
data
object
Object containing transaction identifiers.
data.merchantTransactionId
string
Your unique identifier for the transaction. Use this with systemTransactionId to retrieve full authorisation details from Unity backend.
data.systemTransactionId
string
The system's unique identifier for the transaction. Use this with merchantTransactionId to retrieve full authorisation details from Unity backend.

onError

The onError callback receives error information when the payout fails.

{
  errorCode: "NOT_AUTHENTICATED",
  errorReason: "Invalid or missing credentials.",
  httpStatusCode: 401,
  correlationId: "69ba98c0-ecd8-4c19-8e17-bcbf61e66a24",
  details: []
}
ParameterDescription
error
object
The error object containing details about what went wrong.
error.correlationId
string
Optional. The correlation ID for tracking the error.
error.details
array
Optional. Array of additional error details.
error.errorCode
string
The error code identifier (e.g., "NOT_AUTHENTICATED").
error.errorReason
string
Human-readable error reason (e.g., "Invalid or missing credentials.").
error.httpStatusCode
number
The HTTP status code of the response.