Skip to content

Withdrawal flow

Send payouts to customers using PayPal account credentials provided by your backend.

Overview

The withdrawal flow is designed for customers whose PayPal account credentials your backend provides. This provides a streamlined experience — the customer simply reviews the payout details and confirms.

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

Payout flow

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

Step 1: Display payout details

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

Step 2: Submission

The customer taps the "Withdraw with PayPal" button. The SDK validates the payout configuration and PayPal account details before proceeding. If validation fails, onError is triggered.

Step 3: Payout approval (SDK-managed mode)

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

Return a PrePayoutSubmitResult with isApproved: true to proceed with the payout, or return nil to cancel.

Step 4: Payout execution

For SDK-managed mode, the SDK automatically sends the payout request to the PXP gateway. For backend-managed 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 navigate the customer to a confirmation screen.

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.
  3. Have PayPal account credentials provided by your backend.

Step 1: Configure your SDK

Set up your SDK configuration with PayPal account credentials from your backend to trigger the withdrawal flow.

import PXPCheckoutSDK

// Get session data from your backend
let sessionData = try await fetchSessionData()

// Get customer wallet credentials from your backend
let customerWallet = try await getCustomerWalletFromBackend(userId: currentUserId)

// Configure transaction data for payout
let transactionData = TransactionData(
    amount: Decimal(150.00),
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(
        card: nil,
        paypal: .payout
    ),
    merchantTransactionId: "payout-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

// Configure PayPal with wallet credentials from backend (withdrawal flow)
let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: customerWallet.email,       // PayPal email from backend
            payerId: customerWallet.payerId,   // Required: payer ID from backend
            proceedPayoutWithSdk: true
        )
    )
)

// Initialise the SDK
let checkoutConfig = CheckoutConfig(
    environment: .test,  // Use .live for production
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "customer-123",
    ownerId: "your-owner-id",
    paypalConfig: paypalConfig
)

let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)

The withdrawal flow requires PayPal account credentials provided by your backend in PayPalWallet:

  • payerId is required for payout execution
  • email is optional but recommended for display purposes in the receiver component
// Configure with credentials from your backend
PayPalWallet(
    email: "user@example.com",  // Optional, for display
    payerId: "PAYERID123",       // Required for payout
    proceedPayoutWithSdk: true
)

Step 2: Create the components

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

The submission component automatically reads data from the amount and receiver components when the user taps "Withdraw":

  • Amount value from PayoutAmountComponent
  • Receiver email from PayPalPayoutReceiverComponent
  • Payer ID from PayPalWallet configuration

You don't need to manually wire these components together - the SDK handles data collection automatically.

// Create the amount display component
let amountComponent = try pxpCheckout.create(
    .payoutAmount,
    componentConfig: PayoutAmountComponentConfig(
        label: "Withdrawal Amount"
    )
)

// Create the PayPal receiver display component
let receiverComponent = try pxpCheckout.create(
    .paypalPayoutReceiver,
    componentConfig: PayPalPayoutReceiverComponentConfig(
        label: "PayPal Account",
        showMaskToggle: true,  // Display a toggle (eye icon) to show/hide email
        applyMask: true        // Start with email masked
    )
)

// Create the payout submission component
let submissionComponent = try pxpCheckout.create(
    .payoutSubmission,
    componentConfig: PayoutSubmissionComponentConfig(
        submitText: "Withdraw with PayPal",
        
        // OPTIONAL: Called when button is clicked (before validation)
        onClick: {
            print("Withdrawal button clicked")
            // Track analytics, show loading state, etc.
        },
        
        // OPTIONAL: Called before payout execution (SDK-managed mode only)
        onPrePayoutSubmit: {
            let confirmed = await showConfirmationDialog()
            return confirmed ? PrePayoutSubmitResult(isApproved: true) : nil
        },
        
        // OPTIONAL: Called when payout completes (SDK-managed mode only)
        onPostPayout: { result in
            print("Payout successful:", result.merchantTransactionId)
            showSuccessMessage("Your withdrawal has been processed!")
            navigateToSuccessScreen(transactionId: result.merchantTransactionId)
        },
        
        // OPTIONAL: Called when user cancels
        onCancel: {
            print("Withdrawal cancelled by user")
            showMessage("No problem! Withdraw when you're ready.")
        },
        
        // OPTIONAL: Called on any error
        onError: { error in
            print("Error occurred:", error.errorMessage)
            showErrorMessage("Something went wrong. Please try again.")
        }
    )
)

Step 3: Render the components

Render the components in your SwiftUI view.

struct WithdrawalFlowView: View {
    @State private var amountComponent: BaseComponent?
    @State private var receiverComponent: BaseComponent?
    @State private var submissionComponent: BaseComponent?
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Withdraw Funds")
                .font(.title)
            
            // Amount display
            if let amountComponent = amountComponent {
                amountComponent.buildContent()
                    .frame(height: 60)
            }
            
            // Receiver display
            if let receiverComponent = receiverComponent {
                receiverComponent.buildContent()
                    .frame(height: 60)
            }
            
            Spacer()
            
            // Submit button
            if let submissionComponent = submissionComponent {
                submissionComponent.buildContent()
                    .frame(height: 50)
            }
        }
        .padding()
        .onAppear {
            initialiseComponents()
        }
    }
    
    private func initialiseComponents() {
        Task {
            do {
                let components = try await createPayoutComponents()
                await MainActor.run {
                    self.amountComponent = components.amount
                    self.receiverComponent = components.receiver
                    self.submissionComponent = components.submission
                }
            } catch {
                print("Failed to initialise: \(error)")
            }
        }
    }
}

Step 4: Handle payout modes

The proceedPayoutWithSdk parameter controls whether the SDK or your backend executes the payout. The default value is false (backend-managed).

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

  1. The customer taps "Withdraw with PayPal".
  2. onPrePayoutSubmit is called for approval.
  3. The SDK executes the payout automatically.
  4. onPostPayout is called with the result.
let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: customerWallet.email,
            payerId: customerWallet.payerId,
            proceedPayoutWithSdk: true  // SDK handles payout execution
        )
    )
)

With proceedPayoutWithSdk: true, both onPrePayoutSubmit and onPostPayout callbacks are triggered. The SDK manages the payout execution after approval.

Step 5: Handle errors

Implement comprehensive error handling for the withdrawal process.

let submissionComponent = try pxpCheckout.create(
    .payoutSubmission,
    componentConfig: PayoutSubmissionComponentConfig(
        submitText: "Withdraw with PayPal",
        onPrePayoutSubmit: {
            return await showConfirmationDialog()
        },
        
        onError: { error in
            print("Payout error:", error)
            
            // Handle specific error types
            let userMessage: String
            
            switch error.errorCode {
            case "SDK0803":
                userMessage = "PayPal receiver information is missing."
            case "SDK0805":
                userMessage = "Invalid receiver type. Only email is supported."
            case "SDK0808":
                userMessage = "Invalid PayPal email format."
            case "SDK0809":
                userMessage = "PayPal account information is missing."
            case "SDK0810", "SDK0811", "SDK0812":
                userMessage = "Invalid payout amount. Please contact support."
            case "SDK0817":
                userMessage = "PayPal account identifier is too long."
            case "SDK0818":
                userMessage = "Invalid PayPal account."
            case "SDK0819":
                userMessage = "Payout transaction failed. Please try again."
            default:
                userMessage = "An error occurred. Please try again or contact support."
            }
            
            showErrorAlert(userMessage)
        }
    )
)

For a complete list of error codes including SDK0812-SDK0816 (amount/currency validation), see Data validation.

Example

The following example shows a complete withdrawal flow implementation.

import SwiftUI
import PXPCheckoutSDK

struct PayoutWithdrawalFlowView: View {
    @State private var pxpCheckout: PxpCheckout?
    @State private var amountComponent: BaseComponent?
    @State private var receiverComponent: BaseComponent?
    @State private var submissionComponent: BaseComponent?
    @State private var isLoading = true
    @State private var errorMessage: String?
    @State private var showApprovalAlert = false
    @State private var pendingApprovalContinuation: CheckedContinuation<PrePayoutSubmitResult?, Never>?
    
    let currentUserId: String = "user123"
    var payoutAmount: Double = 0
    var customerEmail: String = ""
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Withdraw Funds")
                .font(.title)
                .fontWeight(.bold)
            
            if isLoading {
                ProgressView("Initialising...")
            } else if let error = errorMessage {
                Text(error)
                    .foregroundColor(.red)
                    .multilineTextAlignment(.center)
            } else {
                // Amount component
                if let amountComponent = amountComponent {
                    amountComponent.buildContent()
                        .frame(height: 60)
                }
                
                // Receiver component
                if let receiverComponent = receiverComponent {
                    receiverComponent.buildContent()
                        .frame(height: 60)
                }
                
                Spacer()
                
                // Submission button
                if let submissionComponent = submissionComponent {
                    submissionComponent.buildContent()
                        .frame(height: 50)
                }
            }
        }
        .padding()
        .alert("Confirm Payout", isPresented: $showApprovalAlert) {
            Button("Cancel", role: .cancel) {
                pendingApprovalContinuation?.resume(returning: nil)
                pendingApprovalContinuation = nil
            }
            Button("Confirm") {
                pendingApprovalContinuation?.resume(returning: 
                    PrePayoutSubmitResult(isApproved: true)
                )
                pendingApprovalContinuation = nil
            }
        } message: {
            Text("Send $\(String(format: "%.2f", payoutAmount)) to \(customerEmail)?")
        }
        .onAppear {
            initialisePayoutFlow()
        }
    }
    
    private func initialisePayoutFlow() {
        Task {
            do {
                isLoading = true
                
                // Get session data from backend
                let sessionResponse = try await getSessionDataFromBackend()
                
                payoutAmount = sessionResponse.payoutAmount
                customerEmail = sessionResponse.customerWallet.email
                
                // Configure transaction data
                let transactionData = TransactionData(
                    amount: Decimal(sessionResponse.payoutAmount),
                    currency: sessionResponse.currency,
                    entryType: .ecom,
                    intent: TransactionIntentData(
                        card: nil,
                        paypal: .payout
                    ),
                    merchantTransactionId: "payout-\(UUID().uuidString)",
                    merchantTransactionDate: { Date() }
                )
                
                // Configure PayPal with wallet credentials from backend
                let paypalConfig = PayPalConfig(
                    payout: PayPalPayoutConfig(
                        paypalWallet: PayPalWallet(
                            email: sessionResponse.customerWallet.email,
                            payerId: sessionResponse.customerWallet.payerId,
                            proceedPayoutWithSdk: true
                        )
                    )
                )
                
                // Initialise SDK
                let checkoutConfig = CheckoutConfig(
                    environment: .test,
                    session: sessionResponse.session,
                    transactionData: transactionData,
                    merchantShopperId: currentUserId,
                    ownerId: "Unity",
                    paypalConfig: paypalConfig
                )
                
                let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
                
                // Create components
                let amountComponent = try pxpCheckout.create(
                    .payoutAmount,
                    componentConfig: PayoutAmountComponentConfig(
                        label: "Withdrawal Amount"
                    )
                )
                
                let receiverComponent = try pxpCheckout.create(
                    .paypalPayoutReceiver,
                    componentConfig: PayPalPayoutReceiverComponentConfig(
                        label: "PayPal Account",
                        showMaskToggle: true,
                        applyMask: true
                    )
                )
                
                let submissionComponent = try pxpCheckout.create(
                    .payoutSubmission,
                    componentConfig: PayoutSubmissionComponentConfig(
                        submitText: "Withdraw with PayPal",
                        
                        onPrePayoutSubmit: { [self] in
                            return await showPayoutApproval()
                        },
                        
                        onPostPayout: { result in
                            handlePayoutSuccess(result)
                        },
                        
                        onCancel: {
                            handleCancellation()
                        },
                        
                        onError: { error in
                            handleError(error)
                        }
                    )
                )
                
                await MainActor.run {
                    self.pxpCheckout = pxpCheckout
                    self.amountComponent = amountComponent
                    self.receiverComponent = receiverComponent
                    self.submissionComponent = submissionComponent
                    self.isLoading = false
                }
                
            } catch {
                await MainActor.run {
                    self.errorMessage = "Failed to initialise: \(error.localizedDescription)"
                    self.isLoading = false
                }
            }
        }
    }
    
    private func showPayoutApproval() async -> PrePayoutSubmitResult? {
        return await withCheckedContinuation { continuation in
            Task { @MainActor in
                self.pendingApprovalContinuation = continuation
                self.showApprovalAlert = true
            }
        }
    }
    
    private func handlePayoutSuccess(_ result: MerchantSubmitResult) {
        print("🎉 Payout successful!")
        print("Merchant TX ID: \(result.merchantTransactionId)")
        print("System TX ID: \(result.systemTransactionId)")
        
        // Show success message and navigate
        // showSuccessMessage("Withdrawal completed successfully!")
        // navigateToSuccessScreen()
    }
    
    private func handleCancellation() {
        print("User cancelled payout")
        // showMessage("No problem! Withdraw when you're ready.")
    }
    
    private func handleError(_ error: BaseSdkException) {
        print("❌ Error: \(error.errorCode) - \(error.errorMessage)")
        errorMessage = "Payout failed: \(error.errorMessage)"
    }
    
    // Backend API call
    
    struct SessionResponse {
        let session: SessionData
        let payoutAmount: Double
        let currency: String
        let customerWallet: (email: String, payerId: String)
    }
    
    private func getSessionDataFromBackend() async throws -> SessionResponse {
        // Call your backend endpoint: POST /api/sessions/payout
        // Returns session data and customer wallet details
        fatalError("Implement your backend call")
    }
}

Callback data

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

onClick

The onClick callback is triggered when the user taps the withdrawal button, before validation begins.

onClick: {
    print("Withdrawal button clicked")
    // Track analytics, show loading state, etc.
}

This callback receives no parameters and is useful for analytics tracking and UI state management.

onPrePayoutSubmit

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

This callback is only triggered when proceedPayoutWithSdk: true. If set to false, this callback won't be called and your backend must handle payout execution independently.

onPrePayoutSubmit: {
    let approved = await showConfirmationDialog()
    return approved ? PrePayoutSubmitResult(isApproved: true) : nil
}
Return propertyDescription
isApproved
Bool
required
Whether to proceed with the payout. Return nil to cancel.

onPostPayout

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

onPostPayout: { result in
    print("Transaction ID:", result.systemTransactionId)
}
ParameterDescription
result
MerchantSubmitResult
Object containing transaction identifiers.
result.merchantTransactionId
String
Your unique identifier for the transaction.
result.systemTransactionId
String
The system's unique identifier for the transaction.

onCancel

The onCancel callback is triggered when the user cancels the payout.

onCancel: {
    print("User cancelled payout")
    showMessage("You can try again anytime.")
}

onError

The onError callback receives error information when the payout fails.

onError: { error in
    print("Error:", error.errorCode, error.errorMessage)
}
ParameterDescription
error
BaseSdkException
The error object containing details about what went wrong.
error.errorCode
String
The error code identifier (e.g., "SDK0803").
error.errorMessage
String
Human-readable error message describing what went wrong.
error.localizedDescription
String
The localised error description (same as errorMessage).

What's next?

Once you've implemented the withdrawal flow, explore these additional resources: