Skip to content

Troubleshooting

Learn how to fix common issues with the PayPal payout components on iOS.

Common issues and solutions

IssueDescriptionNext steps
Payout fails with SDK0809PayoutPayerIdRequiredException - Payer ID is empty or missing.
  • Verify payer ID is passed in onPrePayoutSubmit result.
  • Check account ID is not null or empty string.
  • Implement proper state management for account ID.
Payout fails with SDK0810PayoutAmountInvalidException - Amount is NaN or infinite.
  • Validate payout amount is a valid number.
  • Check for division by zero in amount calculations.
  • Ensure amount is properly formatted as Decimal.
  • Verify currency conversion logic if applicable.
Payout fails with SDK0811PayoutAmountNotPositiveException - Amount must be greater than zero.
  • Ensure payout amount is positive.
  • Check for negative amounts from user input.
  • Validate minimum payout amount ($1.00 USD).
  • Implement input validation before payout.
Payout fails with SDK0817PayoutPayerIdMaxLengthException - Payer ID exceeds 13 characters.
  • Validate payer ID length before payout.
  • Check if account ID was incorrectly formatted.
  • Ensure you're using the correct identifier from PayPal.
  • Contact support if PayPal returns oversized IDs.
Payout fails with SDK0818PayoutPayerIdInvalidException - Payer ID format is invalid.
  • Verify payer ID contains only alphanumeric characters.
  • Check payer ID isn't corrupted or truncated.
  • Ensure proper storage and retrieval of payer ID.
  • Try re-authenticating with PayPal.
Payout fails with SDK0819PayoutFailedException - Payout transaction failed.
  • Check PayPal Business account has sufficient funds.
  • Verify recipient PayPal account exists and is active.
  • Check payout amount is within limits.
  • Review backend logs for detailed error from PayPal.
  • Retry the payout after addressing the underlying issue.
Button not renderingThe PayPal payout button doesn't appear in the SwiftUI view.
  • Verify component is properly created and in view hierarchy.
  • Check SwiftUI view provides adequate space (.frame(height:)).
  • Ensure component reference is stored in @State.
  • Check Console for component creation errors.
  • Verify SDK initialisation completed successfully.
onPrePayoutSubmit not calledThe merchant approval callback isn't triggered before payout submission.
  • This callback is ONLY called when ALL these conditions are met: proceedPayoutWithSdk: true in PayPalConfig, user clicked the payout submission button, and all validation passed (amount, email, payer ID).
  • This callback isn't called when: proceedPayoutWithSdk: false (backend-managed mode), validation errors occurred before approval stage.
  • Verify no errors occurred before this callback.
onPostPayout not calledThe payout completion callback isn't triggered after approval.
  • Verify proceedPayoutWithSdk: true in PayPal config.
  • Check onPrePayoutSubmit returned approval (not nil).
  • Verify payout execution didn't fail (check onError).
  • Check network connectivity during payout.
Network connectivity issuesSDK fails to communicate with PayPal or Unity APIs.
  • Check network reachability using NWPathMonitor.
  • Verify internet connection is available.
  • Handle network errors in onError callback.
  • Implement retry logic for transient failures.
  • Check for VPN or firewall blocking connections.
SSL/TLS certificate errorsPayout fails with SSL errors.
  • **Never ignore SSL errors in production.**
  • Ensure device date/time is correct.
  • Check certificate validity.
  • Test on physical device (simulator may behave differently).
  • Verify App Transport Security settings.
Memory issuesApp crashes or becomes unresponsive during payout flow.
  • Monitor memory using Xcode Instruments.
  • Check for memory leaks in closures.
  • Ensure components are properly released.
  • Avoid retain cycles in callback closures.
  • Use [weak self] in async callbacks.
SwiftUI state issuesComponent state is lost or doesn't update properly.
  • Use @State for component references.
  • Store SDK instance in appropriate scope.
  • Use @StateObject or @ObservedObject if needed.
  • Ensure state updates happen on @MainActor.
  • Check for proper state management in parent views.
Event callbacks not firingonError or other callbacks aren't called.
  • Verify callbacks are defined in configuration.
  • Check Console for error messages.
  • Ensure callbacks are not nil.
  • Add logging to all callbacks to verify execution.
  • Check SDK initialisation completed successfully.
Approval dialog issuesThe approval dialog in onPrePayoutSubmit doesn't work correctly.
  • Ensure proper async/await handling with continuations.
  • Verify CheckedContinuation is properly resumed.
  • Don't resume continuation multiple times.
  • Handle edge cases (user dismisses dialog).
  • Test cancellation flow thoroughly.
Backend token exchange failsAuthorization code exchange returns error.
  • Verify PayPal client credentials are correct.
  • Check backend logs for specific error details.
  • Verify Unity API credentials are valid.
Backend "Token expired" errorPayPal access token has expired when attempting payout.
  • Implement automatic token refresh before expiry.
  • PayPal access tokens expire after 9 hours.
  • Store token expiry time and refresh proactively.
  • Handle refresh token failures gracefully.
Backend "Payout failed - account not found"Payout execution fails because PayPal account can't be found.
  • Verify the payer_id is correct.
  • Ensure the PayPal account exists and is active.
  • Check payer ID wasn't corrupted during storage/retrieval.
Backend "Insufficient permissions"Backend can't execute payouts due to permission issues.
  • Ensure PayPal app has Payouts API enabled in Developer Dashboard.
  • Verify PayPal Business account is fully verified.
  • Check PayPal account has sufficient balance for payouts.
  • Contact PayPal support if permissions are missing.
Simulator vs device differencesPayout flow works in simulator but fails on device (or vice versa).
  • Always test on physical devices for production validation.
  • Check for simulator-specific configurations.
  • Verify code signing and entitlements.
  • Check network conditions on device.
  • Review device Console logs.
App Store rejectionApp rejected due to PayPal payout integration issues.
  • Ensure all required privacy descriptions in Info.plist.
  • Document payout functionality in review notes.
  • Provide test PayPal sandbox accounts for App Review.
  • Test in sandbox environment thoroughly.
  • Ensure proper error handling for edge cases.

Understanding error sources

The SDK can report errors from three different sources. Understanding the distinction helps diagnose issues faster.

SDK validation errors (SDK08xx, SDK09xx)

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

Unity API errors

These occur when communicating with the the Unity backend:

  • Session errors (expired, invalid)
  • Authentication errors
  • Network connectivity issues
  • Forwarded through onError callback with Unity error format

PayPal API errors

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)")
    }
}

Additional error codes

The SDK includes additional validation error codes beyond those in the common issues table:

Payout validation errors

Error codeExceptionDescriptionSolution
SDK0803PaypalPayoutReceiverRequiredExceptionReceiver information is missingEnsure PayPalWallet.email is configured in SDK initialisation
SDK0804PayoutReceiverTypeInvalidExceptionInvalid receiver typeUse .email recipient type for PayPal payouts
SDK0805PaypalPayoutReceiverTypeEmailOnlyExceptionNon-email receiver type usedPayPal payouts only support email recipient type
SDK0806PayoutWalletInvalidExceptionInvalid wallet valueUse .paypal wallet type
SDK0807PayoutWalletRequiresEmailExceptionWallet type requires emailEnsure receiver type is .email when using PayPal wallet
SDK0808PaypalPayoutEmailInvalidExceptionInvalid email formatValidate email format before initialisation
SDK0812PayoutAmountRequiredExceptionAmount is missingEnsure TransactionData.amount is provided
SDK0813PayoutCurrencyRequiredExceptionCurrency code is missingProvide valid currency code in TransactionData
SDK0814PayoutCurrencyLengthInvalidExceptionCurrency code not 3 charactersUse 3-letter ISO 4217 code (e.g., USD, EUR, GBP)
SDK0815PayoutCurrencyCodeInvalidExceptionInvalid ISO 4217 currency codeUse valid ISO 4217 currency code
SDK0816PaypalPayoutEmailMaxLengthExceptionEmail exceeds 127 charactersValidate email length before initialisation

PayPal account setup issues

"Payouts not enabled"

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.

Production issues

Payout failed in production but worked in sandbox

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).

Webhooks not received in production

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.

Logging and debugging

To diagnose issues, enable comprehensive logging.

Console 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)")
    }
)

Console filters

These are suggested filters. The actual subsystem/category names depend on the SDK's logging implementation.

To find actual log identifiers:

  1. Run your app with the SDK integrated.
  2. Open Console.app.
  3. Search for PX or PayPal.
  4. Note the subsystem/category shown in log entries.
  5. 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:YourAppName

Xcode debugging

Enable detailed logging in Xcode:

  1. Open your scheme by going to Product > Scheme > Edit Scheme.
  2. Select Run > Arguments.
  3. Add these environment variables:
    • OS_ACTIVITY_MODE = disable (for cleaner logs)
    • IDEPreferLogStreaming = YES

Useful code snippets

Check network reachability

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 flow

Validate payout configuration

Validate 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 payout

Handle approval dialog with continuation

Properly 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 ...
    }
}

Debug SwiftUI view hierarchy

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
    }
}

Handle async initialisation errors

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")
    }
}

Track payout funnel metrics

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
        ])
    }
)

Testing your integration

For comprehensive testing guidance including sandbox setup, test accounts, test scenarios, and moving to production, see the Testing guide.