Learn how to fix common issues with the PayPal payout components on iOS.
| Issue | Description | Next steps |
|---|---|---|
| Payout fails with SDK0809 | PayoutPayerIdRequiredException - Payer ID is empty or missing. |
|
| Payout fails with SDK0810 | PayoutAmountInvalidException - Amount is NaN or infinite. |
|
| Payout fails with SDK0811 | PayoutAmountNotPositiveException - Amount must be greater than zero. |
|
| Payout fails with SDK0817 | PayoutPayerIdMaxLengthException - Payer ID exceeds 13 characters. |
|
| Payout fails with SDK0818 | PayoutPayerIdInvalidException - Payer ID format is invalid. |
|
| Payout fails with SDK0819 | PayoutFailedException - Payout transaction failed. |
|
| Button not rendering | The PayPal payout button doesn't appear in the SwiftUI view. |
|
| onPrePayoutSubmit not called | The merchant approval callback isn't triggered before payout submission. |
|
| onPostPayout not called | The payout completion callback isn't triggered after approval. |
|
| Network connectivity issues | SDK fails to communicate with PayPal or Unity APIs. |
|
| SSL/TLS certificate errors | Payout fails with SSL errors. |
|
| Memory issues | App crashes or becomes unresponsive during payout flow. |
|
| SwiftUI state issues | Component state is lost or doesn't update properly. |
|
| Event callbacks not firing | onError or other callbacks aren't called. |
|
| Approval dialog issues | The approval dialog in onPrePayoutSubmit doesn't work correctly. |
|
| Backend token exchange fails | Authorization code exchange returns error. |
|
| Backend "Token expired" error | PayPal access token has expired when attempting payout. |
|
| Backend "Payout failed - account not found" | Payout execution fails because PayPal account can't be found. |
|
| Backend "Insufficient permissions" | Backend can't execute payouts due to permission issues. |
|
| Simulator vs device differences | Payout flow works in simulator but fails on device (or vice versa). |
|
| App Store rejection | App rejected due to PayPal payout integration issues. |
|
The SDK can report errors from three different sources. Understanding the distinction helps diagnose issues faster.
These occur before any API call, during client-side validation:
- Caught immediately in SDK
- Can be prevented with proper input validation
- Examples:
SDK0809,SDK0810,SDK0811,SDK0817,SDK0818
These occur when communicating with the the Unity backend:
- Session errors (expired, invalid)
- Authentication errors
- Network connectivity issues
- Forwarded through
onErrorcallback with Unity error format
These occur during actual payout execution:
- Wrapped in
SDK0819(PayoutFailedException) - Original PayPal error included in message
- Examples: insufficient funds, invalid account, blocked recipient
Error handling strategy:
import PXPCheckoutSDK
onError: { error in
switch error.errorCode {
case "SDK0809", "SDK0810", "SDK0811", "SDK0817", "SDK0818":
// SDK validation error - fix input data
print("Validation error: \(error.errorMessage)")
case "SDK0819":
// PayPal API error - check error message for details
print("Payout failed: \(error.errorMessage)")
// Message contains PayPal's error code and reason
default:
// Could be Unity API error or other SDK error
print("Error: \(error.errorCode) - \(error.errorMessage)")
}
}The SDK includes additional validation error codes beyond those in the common issues table:
| Error code | Exception | Description | Solution |
|---|---|---|---|
SDK0803 | PaypalPayoutReceiverRequiredException | Receiver information is missing | Ensure PayPalWallet.email is configured in SDK initialisation |
SDK0804 | PayoutReceiverTypeInvalidException | Invalid receiver type | Use .email recipient type for PayPal payouts |
SDK0805 | PaypalPayoutReceiverTypeEmailOnlyException | Non-email receiver type used | PayPal payouts only support email recipient type |
SDK0806 | PayoutWalletInvalidException | Invalid wallet value | Use .paypal wallet type |
SDK0807 | PayoutWalletRequiresEmailException | Wallet type requires email | Ensure receiver type is .email when using PayPal wallet |
| SDK0808 | PaypalPayoutEmailInvalidException | Invalid email format | Validate email format before initialisation |
SDK0812 | PayoutAmountRequiredException | Amount is missing | Ensure TransactionData.amount is provided |
SDK0813 | PayoutCurrencyRequiredException | Currency code is missing | Provide valid currency code in TransactionData |
SDK0814 | PayoutCurrencyLengthInvalidException | Currency code not 3 characters | Use 3-letter ISO 4217 code (e.g., USD, EUR, GBP) |
SDK0815 | PayoutCurrencyCodeInvalidException | Invalid ISO 4217 currency code | Use valid ISO 4217 currency code |
SDK0816 | PaypalPayoutEmailMaxLengthException | Email exceeds 127 characters | Validate email length before initialisation |
Your PayPal Business account doesn't have payout capability enabled.
Solution: Contact PayPal Support to enable payout capability for your business account. You may need to provide additional business verification documents.
Payouts work in sandbox environment but fail when switching to production.
Causes:
- Live PayPal account not verified
- Insufficient funds in live account
- Payout limits exceeded
- Incorrect live credentials
Solutions:
- Verify your PayPal Business account is fully verified for live transactions.
- Ensure sufficient funds in your PayPal Business account.
- Contact PayPal to increase payout limits if needed.
- Double-check you're using live credentials (not sandbox).
Webhook events are delivered in sandbox but not in production.
Causes:
- Webhook URL not publicly accessible
- SSL certificate issues
- Firewall blocking PayPal requests
Solutions:
- Verify webhook URL is publicly accessible (not localhost).
- Ensure HTTPS with a valid SSL certificate.
- Check firewall rules allow requests from PayPal IP ranges.
- Test with PayPal's webhook simulator in Developer Dashboard.
To diagnose issues, enable comprehensive logging.
Use Console.app or Xcode's console to view logs:
import PXPCheckoutSDK
let config = PayoutSubmissionComponentConfig(
buttonText: "Submit Payout",
// Add verbose logging to callbacks
onClick: {
print("📱 PayPal payout button clicked")
},
onPrePayoutSubmit: {
print("💰 Pre-payout approval requested")
// ... approval logic ...
return result
},
onPostPayout: { result in
print("🎉 Payout successful!")
print(" Merchant TX ID: \(result.merchantTransactionId)")
print(" System TX ID: \(result.systemTransactionId)")
},
onCancel: {
print("❌ User cancelled payout flow")
},
onError: { error in
print("⚠️ Error occurred:")
print(" Code: \(error.errorCode)")
print(" Message: \(error.errorMessage)")
}
)These are suggested filters. The actual subsystem/category names depend on the SDK's logging implementation.
To find actual log identifiers:
- Run your app with the SDK integrated.
- Open Console.app.
- Search for
PXorPayPal. - Note the subsystem/category shown in log entries.
- Use those exact identifiers in your filters.
Use Console.app filters to view specific logs:
# PXP Checkout logs
subsystem:com.pxp.checkout
# PayPal payout logs
process:YourAppName AND payout
# All SDK logs
category:PXPCheckoutSDK
# Example filter once you identify the subsystem:
subsystem:com.yourcompany.yourapp process:YourAppNameEnable detailed logging in Xcode:
- Open your scheme by going to Product > Scheme > Edit Scheme.
- Select Run > Arguments.
- Add these environment variables:
OS_ACTIVITY_MODE=disable(for cleaner logs)IDEPreferLogStreaming=YES
Use this network monitoring implementation before initiating payout flows or when diagnosing network-related errors. The payout SDK requires an active internet connection for payout execution.
import Network
import Combine
class NetworkMonitor: ObservableObject {
static let shared = NetworkMonitor()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
@Published private(set) var isConnected: Bool = false
@Published private(set) var connectionType: NWInterface.InterfaceType?
private init() {}
func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
self?.connectionType = path.availableInterfaces.first?.type
if path.status == .satisfied {
print("✅ Network connected via \(path.availableInterfaces.first?.type.debugDescription ?? "unknown")")
} else {
print("⚠️ Network disconnected")
}
}
}
monitor.start(queue: queue)
}
func stopMonitoring() {
monitor.cancel()
}
deinit {
stopMonitoring()
}
}
// Usage example:
NetworkMonitor.shared.startMonitoring()
if !NetworkMonitor.shared.isConnected {
showAlert(title: "No Connection",
message: "Please check your internet connection and try again.")
return
}
// Proceed with payout flowValidate your payout configuration before creating components to catch errors early.
import PXPCheckoutSDK
struct PayoutValidationResult {
let isValid: Bool
let errors: [String]
}
func validatePayoutConfig(
amount: Decimal,
payerId: String?
) -> PayoutValidationResult {
var errors: [String] = []
// Validate amount
if amount.isNaN {
errors.append("Amount must be a valid number (SDK0810)")
}
if amount <= 0 {
errors.append("Amount must be greater than zero (SDK0811)")
}
if amount < 1.00 {
errors.append("Amount must be at least $1.00 (PayPal minimum)")
}
// Validate payer ID if provided
if let payerId = payerId {
if payerId.isEmpty {
errors.append("Payer ID cannot be empty (SDK0809)")
}
if payerId.count > 13 {
errors.append("Payer ID exceeds 13 characters (SDK0817)")
}
let alphanumericSet = CharacterSet.alphanumerics
if payerId.unicodeScalars.contains(where: { !alphanumericSet.contains($0) }) {
errors.append("Payer ID must be alphanumeric (SDK0818)")
}
}
return PayoutValidationResult(
isValid: errors.isEmpty,
errors: errors
)
}
// Usage example:
let validation = validatePayoutConfig(
amount: Decimal(100.00),
payerId: storedPayerId
)
if !validation.isValid {
print("❌ Validation errors: \(validation.errors.joined(separator: ", "))")
showErrorDialog(validation.errors.first ?? "Validation failed")
return
}
// Proceed with payoutProperly implement async approval dialogs using Swift continuations:
import SwiftUI
import PXPCheckoutSDK
struct PayoutView: View {
@State private var showApprovalAlert = false
@State private var pendingApprovalContinuation: CheckedContinuation<PrePayoutSubmitResult?, Never>?
@State private var storedPayerId: String?
let payoutAmount = 100.00
var body: some View {
VStack {
// ... payout UI ...
}
.alert("Confirm Payout", isPresented: $showApprovalAlert) {
Button("Cancel", role: .cancel) {
// User rejected - resume with nil
pendingApprovalContinuation?.resume(returning: nil)
pendingApprovalContinuation = nil
}
Button("Confirm") {
// User approved - resume with approval result
pendingApprovalContinuation?.resume(returning:
PrePayoutSubmitResult(
isApproved: true,
payerId: storedPayerId ?? ""
)
)
pendingApprovalContinuation = nil
}
} message: {
Text("Send $\(String(format: "%.2f", payoutAmount)) to your PayPal account?")
}
}
private func createPayoutComponent() {
let config = PayoutSubmissionComponentConfig(
buttonText: "Submit Payout",
onPrePayoutSubmit: { [self] in
// Use withCheckedContinuation for async dialog
return await withCheckedContinuation { continuation in
Task { @MainActor in
self.pendingApprovalContinuation = continuation
self.showApprovalAlert = true
}
}
}
)
// ... create component ...
}
}Use these debugging extensions when components don't appear or layout issues occur:
extension View {
func debugPrint(_ message: String) -> some View {
print("🔍 \(message)")
return self
}
func debugBorder(_ color: Color = .red) -> some View {
self.border(color, width: 2)
}
}
// Usage example:
struct PayoutView: View {
var body: some View {
VStack {
if let component = payoutComponent {
component.buildContent()
.frame(height: 50)
.debugBorder(.red) // Visualise button area
.debugPrint("Payout button rendered")
} else {
Text("Loading...")
.debugPrint("Still loading component")
}
}
.debugBorder(.blue) // Visualise container
}
}Implement proper error handling when SDK or component initialisation might fail:
import SwiftUI
import PXPCheckoutSDK
struct PayoutView: View {
@State private var payoutComponent: BaseComponent?
@State private var isLoading = true
@State private var errorMessage: String?
@State private var initTask: Task<Void, Never>?
var body: some View {
VStack {
if isLoading {
ProgressView("Initialising...")
} else if let error = errorMessage {
Text("Error: \(error)")
.foregroundColor(.red)
Button("Retry") {
initTask = Task {
await createPayoutComponent()
}
}
} else if let component = payoutComponent {
component.buildContent()
}
}
.task {
await createPayoutComponent()
}
.onDisappear {
initTask?.cancel()
}
}
@MainActor
private func createPayoutComponent() async {
isLoading = true
errorMessage = nil
do {
// Initialise SDK
let checkoutConfig = CheckoutConfig(
environment: .test,
session: sessionData,
transactionData: transactionData,
merchantShopperId: "user-123",
ownerId: "merchant-456",
paypalConfig: paypalConfig
)
let pxpCheckout = try PxpCheckout.initialize(config: checkoutConfig)
// Create payout component
let config = PayoutSubmissionComponentConfig(
buttonText: "Submit Payout"
)
let component = try pxpCheckout.create(
.payoutSubmission,
componentConfig: config
)
self.payoutComponent = component
} catch let error as NSError {
print("❌ Initialisation error: \(error.localizedDescription)")
print("Error domain: \(error.domain)")
print("Error code: \(error.code)")
print("Error userInfo: \(error.userInfo)")
self.errorMessage = "Failed to initialise payout: \(error.localizedDescription)"
// Track error for analytics
logError(error)
}
isLoading = false
}
func logError(_ error: Error) {
// Log to your analytics/crash reporting service
print("📊 Logging error to analytics")
}
}Monitor your payout funnel to identify drop-off points:
This example uses a placeholder Analytics class. Replace with your actual analytics service (Firebase Analytics, Mixpanel, Amplitude, or your custom service).
import PXPCheckoutSDK
// Example with common analytics services:
// Firebase Analytics:
// Analytics.logEvent("payout_started", parameters: [...])
// Mixpanel:
// Mixpanel.mainInstance().track(event: "payout_started", properties: [...])
// Amplitude:
// Amplitude.instance().logEvent("payout_started", withEventProperties: [...])
// Custom analytics service:
// YourAnalyticsService.shared.trackEvent(name: "payout_started", properties: [...])
let config = PayoutSubmissionComponentConfig(
buttonText: "Submit Payout",
onClick: {
Analytics.track("payout_started", properties: [
"timestamp": Date().timeIntervalSince1970,
"amount": payoutAmount
])
},
onPrePayoutSubmit: {
Analytics.track("payout_approval_requested", properties: [
"timestamp": Date().timeIntervalSince1970
])
let result = await showApprovalDialog()
if result == nil {
Analytics.track("payout_approval_rejected", properties: [
"timestamp": Date().timeIntervalSince1970
])
} else {
Analytics.track("payout_approval_granted", properties: [
"timestamp": Date().timeIntervalSince1970
])
}
return result
},
onPostPayout: { result in
Analytics.track("payout_completed", properties: [
"transactionId": result.merchantTransactionId,
"timestamp": Date().timeIntervalSince1970,
"amount": payoutAmount
])
},
onCancel: {
Analytics.track("payout_cancelled", properties: [
"timestamp": Date().timeIntervalSince1970
])
},
onError: { error in
Analytics.track("payout_error", properties: [
"errorCode": error.errorCode,
"errorMessage": error.errorMessage,
"timestamp": Date().timeIntervalSince1970
])
}
)For comprehensive testing guidance including sandbox setup, test accounts, test scenarios, and moving to production, see the Testing guide.