Send payouts to customers using PayPal account credentials provided by your backend.
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.
The withdrawal flow consists of five key steps for customer payouts.
The customer sees their 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 PayPal account 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 a PrePayoutSubmitResult with isApproved: true to proceed with the payout, or return nil to cancel.
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.
The onPostPayout callback receives the transaction result. You can display a success message and navigate 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.
- Have PayPal account credentials provided by your backend.
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:
payerIdis required for payout executionemailis 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
)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
PayPalWalletconfiguration
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.")
}
)
)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)")
}
}
}
}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:
- The customer taps "Withdraw with PayPal".
onPrePayoutSubmitis called for approval.- The SDK executes the payout automatically.
onPostPayoutis 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.
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.
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")
}
}This section describes the data received by the different callbacks as part of the withdrawal flow.
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.
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 property | Description |
|---|---|
isApprovedBool required | Whether to proceed with the payout. Return nil to cancel. |
The onPostPayout callback receives the payout result when the transaction completes successfully.
onPostPayout: { result in
print("Transaction ID:", result.systemTransactionId)
}| Parameter | Description |
|---|---|
resultMerchantSubmitResult | Object containing transaction identifiers. |
result.merchantTransactionIdString | Your unique identifier for the transaction. |
result.systemTransactionIdString | The system's unique identifier for the transaction. |
The onCancel callback is triggered when the user cancels the payout.
onCancel: {
print("User cancelled payout")
showMessage("You can try again anytime.")
}The onError callback receives error information when the payout fails.
onError: { error in
print("Error:", error.errorCode, error.errorMessage)
}| Parameter | Description |
|---|---|
errorBaseSdkException | The error object containing details about what went wrong. |
error.errorCodeString | The error code identifier (e.g., "SDK0803"). |
error.errorMessageString | Human-readable error message describing what went wrong. |
error.localizedDescriptionString | The localised error description (same as errorMessage). |
Once you've implemented the withdrawal flow, explore these additional resources:
- Payout submission component: Configure the submission button's appearance and behaviour.
- Payout receiver component: Customise the receiver display.
- Payout amount component: Configure the amount display.
- Events: Handle additional payout events and callbacks.
- Testing: Test your payout integration.