Send payouts to returning customers using stored wallet details.
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.
The withdrawal flow consists of five key steps for returning customer payouts.
The customer sees their stored PayPal email displayed alongside the payout amount. The receiver component shows the wallet destination, optionally masked for privacy.
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.
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.
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.
The onPostPayout callback receives the transaction result. You can display a success message and redirect the customer to a confirmation screen.
To use the withdrawal flow for payouts:
- Ensure your PayPal merchant account is onboarded with PXP.
- Have sufficient funds in your gateway balance to cover payout amounts and fees.
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.
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
)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?")
}
}
}
}When proceedPayoutWithSdk = true, the SDK handles the complete flow:
- The customer sees their stored wallet details.
- The customer taps "Withdraw with PayPal".
onPrePayoutSubmitis called for approval.- The SDK executes the payout automatically.
onPostPayoutis called with the result.
paypalConfig = PaypalConfig(
payout = PayoutConfig(
proceedPayoutWithSdk = true, // SDK handles payout execution
paypalWallet = PayPalPayOutWalletConfig(
email = "customer@example.com",
payerId = "PAYER_ID_XXX"
)
)
)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.")
}
}
)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")
}
}
)
}
}
}This section describes the data received by the different callbacks as part of the withdrawal flow.
The onPrePayoutSubmit callback is called before payout execution. Return a PrePayoutSubmitResult object indicating whether to proceed.
onPrePayoutSubmit = {
val approved = showConfirmationDialog()
PrePayoutSubmitResult(isApproved = approved)
}| Return Property | Description |
|---|---|
isApprovedBoolean required | Whether to proceed with the payout. Return false to cancel. |
The onPostPayout callback receives the payout result when the transaction completes successfully.
onPostPayout = { result ->
Log.d("Payout", "Transaction details: ${result.merchantTransactionId}, ${result.systemTransactionId}")
}| Parameter | Description |
|---|---|
resultPostPayoutResult required | Object containing transaction identifiers. |
result.merchantTransactionIdString? | Your unique identifier for the transaction. Use this with systemTransactionId to retrieve full authorisation details from Unity backend. |
result.systemTransactionIdString required | The system's unique identifier for the transaction. Use this with merchantTransactionId to retrieve full authorisation details from Unity backend. |
The onError callback receives error information when the payout fails.
onError = { error ->
Log.e("Payout", "Error: ${error.errorReason}")
}| Parameter | Description |
|---|---|
errorPayOutError required | The error object containing details about what went wrong. |
error.correlationIdString? | The correlation ID for tracking the error. |
error.detailsList<String?>? | List of additional error details. |
error.errorCodeString? | The error code identifier (e.g., "PO04"). |
error.errorReasonString? | Human-readable error reason (e.g., "Invalid recipient information."). |
error.httpStatusCodeInt? | The HTTP status code of the response. |
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.