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 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 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 PrePayoutSubmitResult(isApproved = true) to proceed with the payout, or PrePayoutSubmitResult(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 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.

Step 1: Initialise the SDK

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

import com.pxp.checkout.PxpCheckout
import com.pxp.checkout.models.*
import java.util.UUID

val pxpCheckout = PxpCheckout.builder()
    .withConfig(
        PxpSdkConfig(
            environment = Environment.TEST, // or Environment.LIVE for production
            session = sessionData,
            ownerId = "your-owner-id",
            ownerType = "MerchantGroup",
            clientId = "your-client-id",
            transactionData = TransactionData(
                amount = 100.0,
                currency = "USD",
                merchant = "your-merchant-id",
                entryType = EntryType.Ecom,
                intent = TransactionIntentData(paypal = IntentType.Payout),
                merchantTransactionId = UUID.randomUUID().toString(),
                merchantTransactionDate = { System.currentTimeMillis() }
            ),
            paypalConfig = PaypalConfig(
                payout = PayoutConfig(
                    proceedPayoutWithSdk = true,
                    paypalWallet = PayPalPayOutWalletConfig(
                        email = "customer@example.com",  // Stored PayPal email
                        payerId = "PAYER_ID_XXX"         // Required: stored payer ID
                    )
                )
            )
        )
    )
    .withContext(context)
    .build()

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

Step 2: Create the components

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

import com.pxp.checkout.components.payoutamount.PayoutAmountComponentConfig
import com.pxp.checkout.components.paypalpayoutreceiver.PaypalPayoutReceiverComponentConfig
import com.pxp.checkout.components.payoutsubmission.PayoutSubmissionComponentConfig
import com.pxp.checkout.models.payout.PrePayoutSubmitResult
import com.pxp.checkout.types.ComponentType

// Create the amount display component
val amountComponent = pxpCheckout.createComponent(
    type = ComponentType.PAYOUT_AMOUNT,
    config = PayoutAmountComponentConfig(
        label = "Withdrawal Amount"
    )
)

// Create the PayPal receiver display component
val receiverComponent = pxpCheckout.createComponent(
    type = ComponentType.PAYPAL_PAYOUT_RECEIVER,
    config = PaypalPayoutReceiverComponentConfig(
        label = "PayPal Account",
        showMaskToggle = true,  // Display a toggle to allow the customer to show or hide their full email
        applyMask = true        // Start with masked email
    )
)

// Create the submission button for PayPal
val submitConfig = PayoutSubmissionComponentConfig(
    recipientWallet = "Paypal",
    
    // OPTIONAL: Called before payout execution
    onPrePayoutSubmit = {
        val confirmed = showConfirmationDialog()
        PrePayoutSubmitResult(isApproved = confirmed)
    },
    
    // OPTIONAL: Called when the payout completes successfully
    onPostPayout = { result ->
        Log.d("Payout", "Payout successful: ${result.systemTransactionId}")
        showSuccessMessage("Your withdrawal has been processed!")
        navigateToSuccessScreen(result.merchantTransactionId)
    },
    
    // OPTIONAL: Called on any error
    onError = { error ->
        Log.e("Payout", "Payout failed: ${error.errorReason}")
        showErrorMessage("Withdrawal failed. Please try again.")
    }
)

val submitComponent = pxpCheckout.createComponent(
    type = ComponentType.PAYOUT_SUBMISSION,
    config = submitConfig
)

Step 3: Render the components

Compose the components in your UI using Jetpack Compose.

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun WithdrawalScreen() {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = MaterialTheme.colorScheme.background
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "Withdraw funds",
                style = MaterialTheme.typography.headlineMedium
            )
            
            Text(
                text = "Review your withdrawal details below",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
            
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(
                    containerColor = MaterialTheme.colorScheme.surfaceVariant
                )
            ) {
                Column(
                    modifier = Modifier.padding(16.dp),
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    // Amount component
                    pxpCheckout.buildComponentView(
                        component = amountComponent,
                        modifier = Modifier.fillMaxWidth()
                    )
                    
                    Divider()
                    
                    // Receiver component
                    pxpCheckout.buildComponentView(
                        component = receiverComponent,
                        modifier = Modifier.fillMaxWidth()
                    )
                    
                    Divider()
                    
                    // Submit button
                    pxpCheckout.buildComponentView(
                        component = submitComponent,
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            }
            
            TextButton(
                onClick = { navigateToChangePayoutMethod() }
            ) {
                Text("Use a different PayPal account?")
            }
        }
    }
}

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 taps "Withdraw with PayPal".
  3. onPrePayoutSubmit is called for approval.
  4. The SDK executes the payout automatically.
  5. onPostPayout is called with the result.
paypalConfig = PaypalConfig(
    payout = PayoutConfig(
        proceedPayoutWithSdk = true,  // SDK handles payout execution
        paypalWallet = PayPalPayOutWalletConfig(
            email = "customer@example.com",
            payerId = "PAYER_ID_XXX"
        )
    )
)

Step 5: Handle errors

Implement comprehensive error handling for the payout process.

val submitConfig = PayoutSubmissionComponentConfig(
    recipientWallet = "Paypal",
    
    onError = { error ->
        Log.e("Payout", "Payout error: ${error.errorReason}")
        
        // Handle specific error types
        when (error.errorCode) {
            "PO04" -> showError("Invalid recipient information.")
            "PO02" -> showError("Invalid payout amount. Please contact support.")
            "PO03" -> showError("Invalid currency.")
            "PO07" -> showError("Invalid transaction date.")
            else -> showError("An error occurred. Please try again or contact support.")
        }
    }
)

Example

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

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pxp.checkout.PxpCheckout
import com.pxp.checkout.components.payoutamount.PayoutAmountComponentConfig
import com.pxp.checkout.components.paypalpayoutreceiver.PaypalPayoutReceiverComponentConfig
import com.pxp.checkout.components.payoutsubmission.PayoutSubmissionComponentConfig
import com.pxp.checkout.models.*
import com.pxp.checkout.models.payout.PrePayoutSubmitResult
import com.pxp.checkout.types.ComponentType
import java.util.UUID

class WithdrawalActivity : ComponentActivity() {
    
    private lateinit var pxpCheckout: PxpCheckout
    private val payoutAmount = 150.0
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Get session data from your backend
        val sessionData = getSessionDataFromBackend()
        
        // Get stored customer wallet details
        val customerWallet = getStoredCustomerWallet()
        
        // Initialise SDK with stored wallet details (withdrawal flow)
        pxpCheckout = PxpCheckout.builder()
            .withConfig(
                PxpSdkConfig(
                    environment = Environment.TEST,
                    session = sessionData,
                    ownerId = "Unity",
                    ownerType = "MerchantGroup",
                    clientId = "your-client-id",
                    transactionData = TransactionData(
                        amount = payoutAmount,
                        currency = "USD",
                        merchant = "your-merchant-id",
                        entryType = EntryType.Ecom,
                        intent = TransactionIntentData(paypal = IntentType.Payout),
                        merchantTransactionId = UUID.randomUUID().toString(),
                        merchantTransactionDate = { System.currentTimeMillis() }
                    ),
                    paypalConfig = PaypalConfig(
                        payout = PayoutConfig(
                            proceedPayoutWithSdk = true,
                            paypalWallet = PayPalPayOutWalletConfig(
                                email = customerWallet.email,
                                payerId = customerWallet.payerId
                            )
                        )
                    )
                )
            )
            .withContext(this)
            .build()
        
        setContent {
            MaterialTheme {
                WithdrawalScreen()
            }
        }
    }
    
    @Composable
    fun WithdrawalScreen() {
        var payoutStatus by remember { mutableStateOf<String?>(null) }
        var showSuccessDialog by remember { mutableStateOf(false) }
        
        // Create amount display component
        val amountComponent = remember {
            pxpCheckout.createComponent(
                type = ComponentType.PAYOUT_AMOUNT,
                config = PayoutAmountComponentConfig(
                    label = "Withdrawal Amount"
                )
            )
        }
        
        // Create receiver display component
        val receiverComponent = remember {
            pxpCheckout.createComponent(
                type = ComponentType.PAYPAL_PAYOUT_RECEIVER,
                config = PaypalPayoutReceiverComponentConfig(
                    label = "Sending to",
                    showMaskToggle = true,
                    applyMask = true
                )
            )
        }
        
        // Create submission button
        val submitConfig = remember {
            PayoutSubmissionComponentConfig(
                recipientWallet = "Paypal",
                
                onClick = {
                    trackEvent("payout_button_tapped")
                },
                
                onPrePayoutSubmit = {
                    // Perform validation
                    val balance = checkUserBalance()
                    if (balance < payoutAmount) {
                        showError("Insufficient balance for this withdrawal.")
                        return@PayoutSubmissionComponentConfig PrePayoutSubmitResult(isApproved = false)
                    }
                    
                    // Show confirmation dialog
                    val confirmed = showConfirmationDialog(
                        title = "Confirm Withdrawal",
                        message = "Withdraw $${"%.2f".format(payoutAmount)} to your PayPal account?",
                        details = listOf(
                            "Processing time: 1-2 business days"
                        )
                    )
                    
                    if (!confirmed) {
                        trackEvent("payout_cancelled_by_user")
                        PrePayoutSubmitResult(isApproved = false)
                    } else {
                        trackEvent("payout_approved")
                        PrePayoutSubmitResult(isApproved = true)
                    }
                },
                
                onPostPayout = { result ->
                    Log.d("Payout", "Payout successful: $result")
                    
                    trackEvent("payout_completed", mapOf(
                        "merchantTransactionId" to result.merchantTransactionId,
                        "systemTransactionId" to result.systemTransactionId,
                        "amount" to payoutAmount
                    ))
                    
                    // Update local balance
                    updateUserBalance(balance - payoutAmount)
                    
                    payoutStatus = "Withdrawal completed successfully!"
                    showSuccessDialog = true
                },
                
                onError = { error ->
                    Log.e("Payout", "Payout error: ${error.errorReason}")
                    
                    trackEvent("payout_failed", mapOf(
                        "errorCode" to error.errorCode,
                        "httpStatusCode" to error.httpStatusCode.toString()
                    ))
                    
                    payoutStatus = "Error: ${error.errorReason}"
                }
            )
        }
        
        val submitComponent = remember {
            pxpCheckout.createComponent(
                type = ComponentType.PAYOUT_SUBMISSION,
                config = submitConfig
            )
        }
        
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                Text(
                    text = "Withdraw funds",
                    style = MaterialTheme.typography.headlineMedium
                )
                
                Text(
                    text = "Review your withdrawal details below",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
                
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.surfaceVariant
                    )
                ) {
                    Column(
                        modifier = Modifier.padding(16.dp),
                        verticalArrangement = Arrangement.spacedBy(12.dp)
                    ) {
                        // Amount
                        pxpCheckout.buildComponentView(
                            component = amountComponent,
                            modifier = Modifier.fillMaxWidth()
                        )
                        
                        Divider()
                        
                        // Receiver
                        pxpCheckout.buildComponentView(
                            component = receiverComponent,
                            modifier = Modifier.fillMaxWidth()
                        )
                        
                        Divider()
                        
                        // Submit button
                        pxpCheckout.buildComponentView(
                            component = submitComponent,
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
                
                TextButton(
                    onClick = { navigateToChangePayoutMethod() }
                ) {
                    Text("Use a different PayPal account?")
                }
                
                // Status message
                payoutStatus?.let { status ->
                    Text(
                        text = status,
                        style = MaterialTheme.typography.bodyMedium,
                        color = if (status.contains("Error")) {
                            MaterialTheme.colorScheme.error
                        } else {
                            MaterialTheme.colorScheme.primary
                        }
                    )
                }
            }
        }
        
        // Success dialog
        if (showSuccessDialog) {
            AlertDialog(
                onDismissRequest = { showSuccessDialog = false },
                title = { Text("Withdrawal Successful") },
                text = { Text(payoutStatus ?: "") },
                confirmButton = {
                    Button(onClick = { 
                        showSuccessDialog = false
                        finish()
                    }) {
                        Text("OK")
                    }
                }
            )
        }
    }
}

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 a PrePayoutSubmitResult object indicating whether to proceed.

onPrePayoutSubmit = {
    val approved = showConfirmationDialog()
    PrePayoutSubmitResult(isApproved = approved)
}
Return PropertyDescription
isApproved
Boolean
required
Whether to proceed with the payout. Return false to cancel.

onPostPayout

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

onPostPayout = { result ->
    Log.d("Payout", "Transaction details: ${result.merchantTransactionId}, ${result.systemTransactionId}")
}
ParameterDescription
result
PostPayoutResult
required
Object containing transaction identifiers.
result.merchantTransactionId
String?
Your unique identifier for the transaction. Use this with systemTransactionId to retrieve full authorisation details from Unity backend.
result.systemTransactionId
String
required
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.

onError = { error ->
    Log.e("Payout", "Error: ${error.errorReason}")
}
ParameterDescription
error
PayOutError
required
The error object containing details about what went wrong.
error.correlationId
String?
The correlation ID for tracking the error.
error.details
List<String?>?
List of additional error details.
error.errorCode
String?
The error code identifier (e.g., "PO04").
error.errorReason
String?
Human-readable error reason (e.g., "Invalid recipient information.").
error.httpStatusCode
Int?
The HTTP status code of the response.

What's next?

Now that you've implemented the withdrawal flow, explore these related topics:

  • Configuration: Customise component appearance and behavior.
  • Events: Handle additional payout events and callbacks.
  • Testing: Test your payout integration.