Skip to content

Testing

Test your payout integration thoroughly before going live.

Overview

Always test payout functionality in PayPal's sandbox environment before deploying to production.

This guide covers:

  • Setting up sandbox accounts
  • Testing the complete payout flow
  • Common test scenarios
  • Troubleshooting failed tests
  • Moving to production

Before you start

Ensure you have:

Step 1: Configure your sandbox environment

Update iOS app configuration

Import the SDK and set it to use the test environment:

import PXPCheckoutSDK

let config = CheckoutConfig(
    environment: .test,  // Environment.test (sandbox)
    session: sessionData,
    transactionData: transactionData,
    merchantShopperId: "test_shopper_01",
    ownerId: "Unity"
)

Available environment values:

  • .test: Sandbox environment for testing
  • .live: Production environment

The SDK automatically uses sandbox PayPal endpoints and the test Unity API when environment: .test is set.

Update backend configuration

This is backend server configuration (not iOS code):

// Environment configuration
const ENVIRONMENT = 'sandbox';

const config = {
  paypal: {
    clientId: process.env.PAYPAL_SANDBOX_CLIENT_ID,
    secret: process.env.PAYPAL_SANDBOX_SECRET,
    apiBaseUrl: 'https://api-m.sandbox.paypal.com'
  },
  unity: {
    apiBaseUrl: 'https://api-services-test.pxp.io',
    clientId: process.env.UNITY_TEST_CLIENT_ID,
    tokenId: process.env.UNITY_TEST_TOKEN_ID,
    tokenValue: process.env.UNITY_TEST_TOKEN_VALUE
  }
};

Step 2: Create test accounts

Create a business account (payout sender)

  1. Log in to PayPal Developer Dashboard.
  2. Go to Sandbox > Accounts.
  3. Click Create Account.
  4. Configure your account:
    • Account Type: Business
    • Email: business-sender@example.com
    • Password: Choose a password
    • Account balance: $10,000 (for testing)
    • Country: United States
  5. Click Create Account.

Create personal accounts (payout recipients)

Create multiple recipient accounts to test different scenarios:

Account 1: Valid recipient

  • Account type: Personal
  • Email: recipient1@example.com
  • Balance: $0
  • Status: Verified

Account 2: Unverified recipient

  • Account type: Personal
  • Email: recipient2@example.com
  • Balance: $0
  • Status: Unverified (don't complete email verification)

Account 3: Limited account

  • Account type: Personal
  • Email: recipient3@example.com
  • Balance: $0
  • Status: Limited (simulate by restricting in sandbox)

Access test accounts

To log in to sandbox accounts:

  1. Go to PayPal Sandbox.
  2. Use the sandbox account credentials.
  3. Verify account status and balance.

Step 3: Test payout execution

Test scenario: successful payout (SDK-managed)

Configuration: proceedPayoutWithSdk: true

  1. Initiate the withdrawal flow with pre-configured payer ID.
  2. Verify: onPrePayoutSubmit callback triggered.
  3. Show approval UI: Display confirmation to user.
  4. Return approval: isApproved: true with payer ID.
  5. Observe: SDK executes payout.
  6. Wait: For payout to process (typically < 30 seconds in sandbox).
  7. Verify: onPostPayout callback triggered with transaction IDs.
  8. Check sandbox: Log into recipient1@example.com account.
  9. Verify balance: $100 added to account balance.

Expected result: The payout completes and funds appear in the recipient account.

Test scenario: Merchant rejects payout

  1. Initiate the withdrawal flow.
  2. In onPrePayoutSubmit callback, show the approval UI.
  3. Return rejection: isApproved: false.
  4. Verify: No payout executed.
  5. Check: No error callback triggered.

Expected result: The payout is cancelled gracefully and no funds are transferred.

Test scenario: Successful payout (backend-managed)

Configuration: proceedPayoutWithSdk: false

  1. Initiate the withdrawal flow with pre-configured credentials.
  2. Call the backend endpoint: POST /api/payouts/execute.
  3. Verify: Backend calls the Unity Payout API.
  4. Check response: Payout transaction ID returned.
  5. Log into sandbox: Verify funds transferred.

Expected result: Backend successfully triggers payout.

Step 4: Test error scenarios

Test: Invalid payer ID format

// Configure SDK with invalid payer ID (contains non-alphanumeric characters)
let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: "user@example.com",
            payerId: "INVALID@ID!",  // Contains non-alphanumeric characters
            proceedPayoutWithSdk: true
        )
    )
)

Expected result: onError callback with error code SDK0818 (PayoutPayerIdInvalidException).

To test an ID that is valid format but doesn't exist in PayPal, the SDK will pass validation but the payout transaction will fail with error code SDK0819 (PayoutFailedException).

Test: Empty payer ID

// Configure SDK with empty payer ID
let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: "user@example.com",
            payerId: "",  // Empty payer ID
            proceedPayoutWithSdk: true
        )
    )
)

Expected result: onError callback with error code SDK0809 (PayoutPayerIdRequiredException)

Test: Payer ID too long

// Configure SDK with payer ID exceeding max length
let paypalConfig = PayPalConfig(
    payout: PayPalPayoutConfig(
        paypalWallet: PayPalWallet(
            email: "user@example.com",
            payerId: "ABCDEFGHIJKLMN",  // 14 characters, max is 13
            proceedPayoutWithSdk: true
        )
    )
)

Expected result: onError callback with error code SDK0817 (PayoutPayerIdMaxLengthException)

Test: Negative amount

let transactionData = TransactionData(
    amount: Decimal(-50.00),  // Negative amount
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .payout),
    merchantTransactionId: "test-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

Expected result: onError callback with error code SDK0811 (PayoutAmountNotPositiveException)

Test: Zero amount

let transactionData = TransactionData(
    amount: Decimal(0.00),  // Zero amount
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .payout),
    merchantTransactionId: "test-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

Expected result: onError callback with error code SDK0811 (PayoutAmountNotPositiveException) - amount must be greater than zero

Test: Invalid amount (NaN or infinity)

let transactionData = TransactionData(
    amount: Decimal(Double.nan),  // Not a number
    currency: "USD",
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .payout),
    merchantTransactionId: "test-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

Expected result: onError callback with error code SDK0810 (PayoutAmountInvalidException)

Test: Invalid currency code length

let transactionData = TransactionData(
    amount: Decimal(100.00),
    currency: "US",  // Only 2 characters, must be 3
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .payout),
    merchantTransactionId: "test-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

Expected result: onError callback with error code SDK0814 (PayoutCurrencyInvalidLengthException).

Test: Invalid currency code format

let transactionData = TransactionData(
    amount: Decimal(100.00),
    currency: "XXX",  // Invalid ISO 4217 code
    entryType: .ecom,
    intent: TransactionIntentData(card: nil, paypal: .payout),
    merchantTransactionId: "test-\(UUID().uuidString)",
    merchantTransactionDate: { Date() }
)

Expected result: onError callback with error code SDK0815 (PayoutCurrencyInvalidException).

Test: Session expired

  1. Create a session.
  2. Wait for the session timeout (typically 2 hours).
  3. Attempt a payout.
  4. Verify: Unity API returns session error.

Expected result: onError callback with Unity API error response (not SDK error code)

Session expiry is validated by the the the Unity backend, not the iOS SDK. The SDK will forward the API error. We recommend prompting the customer to refresh the session or re-authenticate.

Test: Insufficient funds (merchant account)

Setup: Configure your sandbox PayPal business account (the payout sender) with $0 balance.

  1. Attempt payout of $100 to recipient.
  2. Verify: PayPal API rejects payout due to insufficient merchant funds.
  3. SDK behaviour: Error forwarded as SDK0819 (PayoutFailedException).

Expected result: onError callback with error code SDK0819 and PayPal error details in message.

This tests your business account balance, not the recipient's balance.

Step 5: Test webhook handling (backend only)

Webhooks are received by your backend server, not the iOS app. This testing is for backend integration. The iOS SDK doesn't interact with webhooks directly. Webhook processing happens entirely on your backend.

If you implemented backend webhooks:

Test: Payout success webhook

  1. Execute a payout.
  2. Wait: For webhook delivery (1-5 minutes in sandbox).
  3. Check backend: Webhook received.
  4. Verify: Event type is PAYMENT.PAYOUTS-ITEM.SUCCEEDED.
  5. Verify: Signature validation passes.
  6. Check: Payout status updated in database.

Expected result: Webhook received and processed correctly.

Test: Payout failure webhook

  1. Trigger payout that will fail (e.g., to blocked account).
  2. Wait for webhook.
  3. Verify: Event type is PAYMENT.PAYOUTS-ITEM.FAILED.
  4. Check: Error handling logic executes.

Expected result: Failure webhook handled correctly.

Simulate webhooks

Use PayPal's webhook simulator:

  1. Go to Developer Dashboard > Webhooks.
  2. Click on your webhook.
  3. Click "Simulator".
  4. Select event type to simulate.
  5. Click "Send Test".

Step 6: Test edge cases

Multiple sequential payouts

  1. Execute payout #1 to recipient1 ($50).
  2. Immediately execute payout #2 to recipient1 ($50).
  3. Verify: Both complete successfully.
  4. Check balance: $100 total received.

Concurrent payouts

  1. Execute payout to recipient1 ($50).
  2. Simultaneously execute payout to recipient2 ($50).
  3. Verify: Both complete successfully.

Network interruption

  1. Start payout process.
  2. Disable network mid-transaction.
  3. Verify: Error handling activates.
  4. Check: Transaction not partially completed.

Test checklist

Use this checklist to ensure comprehensive testing:

Payout execution

  • Successful payout (SDK-managed)
  • Successful payout (backend-managed)
  • Merchant approval flow
  • Merchant rejection
  • Invalid payer ID
  • Empty payer ID
  • Payer ID too long
  • Invalid amount
  • Negative amount
  • Zero amount
  • Invalid currency
  • Session expired

Error handling

  • Network errors
  • API errors
  • Validation errors
  • User-facing error messages
  • Error logging

Integration

  • Backend API calls
  • Session creation
  • Webhook delivery (if implemented)
  • Webhook signature validation

User experience

  • Button rendering
  • Loading states
  • Success messages
  • Error messages
  • Cancellation flow
  • Retry options

Moving to production

Once all tests pass in sandbox:

Step 1: Update PayPal credentials

Replace sandbox credentials with live credentials:

const config = {
  paypal: {
    clientId: process.env.PAYPAL_LIVE_CLIENT_ID,
    secret: process.env.PAYPAL_LIVE_SECRET,
    apiBaseUrl: 'https://api-m.paypal.com'
  }
};

Step 2: Update Unity environment

let config = CheckoutConfig(
    environment: .live,  // Switch to live
    // ...
)
const config = {
  unity: {
    apiBaseUrl: 'https://api-services.pxp.io',
    clientId: process.env.UNITY_LIVE_CLIENT_ID,
    tokenId: process.env.UNITY_LIVE_TOKEN_ID,
    tokenValue: process.env.UNITY_LIVE_TOKEN_VALUE
  }
};

Step 3: Test with small amounts

Before full rollout:

  1. Execute test payout of $1.00 to your own PayPal account.
  2. Verify funds received.
  3. Verify transaction appears in PayPal reports.
  4. Test refund flow if applicable.

Step 4: Enable monitoring

  • Set up error tracking (e.g., Sentry, Datadog).
  • Configure alerts for payout failures.
  • Monitor webhook delivery rates.
  • Track payout completion times.

Step 5: Gradual rollout

  1. Beta users: Release to small group (5-10%).
  2. Monitor: Watch for errors and user feedback.
  3. Iterate: Fix any issues discovered.
  4. Expand: Gradually increase rollout percentage.
  5. Full release: Roll out to all users.

Troubleshooting

If you encounter issues during testing, see the Troubleshooting guide for solutions to common problems including production vs sandbox issues, webhook delivery problems, and SDK error codes.