Test your payout integration thoroughly before going live.
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
Ensure you have:
- Completed PayPal onboarding with sandbox credentials.
- Implemented the withdrawal flow including backend setup.
- Created PayPal sandbox test accounts.
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.
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
}
};- Log in to PayPal Developer Dashboard.
- Go to Sandbox > Accounts.
- Click Create Account.
- Configure your account:
- Account Type: Business
- Email: business-sender@example.com
- Password: Choose a password
- Account balance: $10,000 (for testing)
- Country: United States
- Click Create Account.
Create multiple recipient accounts to test different scenarios:
- Account type: Personal
- Email:
recipient1@example.com - Balance: $0
- Status: Verified
- Account type: Personal
- Email:
recipient2@example.com - Balance: $0
- Status: Unverified (don't complete email verification)
- Account type: Personal
- Email:
recipient3@example.com - Balance: $0
- Status: Limited (simulate by restricting in sandbox)
To log in to sandbox accounts:
- Go to PayPal Sandbox.
- Use the sandbox account credentials.
- Verify account status and balance.
Configuration: proceedPayoutWithSdk: true
- Initiate the withdrawal flow with pre-configured payer ID.
- Verify:
onPrePayoutSubmitcallback triggered. - Show approval UI: Display confirmation to user.
- Return approval:
isApproved: truewith payer ID. - Observe: SDK executes payout.
- Wait: For payout to process (typically < 30 seconds in sandbox).
- Verify:
onPostPayoutcallback triggered with transaction IDs. - Check sandbox: Log into
recipient1@example.comaccount. - Verify balance: $100 added to account balance.
Expected result: The payout completes and funds appear in the recipient account.
- Initiate the withdrawal flow.
- In
onPrePayoutSubmitcallback, show the approval UI. - Return rejection:
isApproved: false. - Verify: No payout executed.
- Check: No error callback triggered.
Expected result: The payout is cancelled gracefully and no funds are transferred.
Configuration: proceedPayoutWithSdk: false
- Initiate the withdrawal flow with pre-configured credentials.
- Call the backend endpoint:
POST /api/payouts/execute. - Verify: Backend calls the Unity Payout API.
- Check response: Payout transaction ID returned.
- Log into sandbox: Verify funds transferred.
Expected result: Backend successfully triggers payout.
// 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).
// 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)
// 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)
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)
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
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)
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).
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).
- Create a session.
- Wait for the session timeout (typically 2 hours).
- Attempt a payout.
- 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.
Setup: Configure your sandbox PayPal business account (the payout sender) with $0 balance.
- Attempt payout of $100 to recipient.
- Verify: PayPal API rejects payout due to insufficient merchant funds.
- 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.
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:
- Execute a payout.
- Wait: For webhook delivery (1-5 minutes in sandbox).
- Check backend: Webhook received.
- Verify: Event type is
PAYMENT.PAYOUTS-ITEM.SUCCEEDED. - Verify: Signature validation passes.
- Check: Payout status updated in database.
Expected result: Webhook received and processed correctly.
- Trigger payout that will fail (e.g., to blocked account).
- Wait for webhook.
- Verify: Event type is
PAYMENT.PAYOUTS-ITEM.FAILED. - Check: Error handling logic executes.
Expected result: Failure webhook handled correctly.
Use PayPal's webhook simulator:
- Go to Developer Dashboard > Webhooks.
- Click on your webhook.
- Click "Simulator".
- Select event type to simulate.
- Click "Send Test".
- Execute payout #1 to recipient1 ($50).
- Immediately execute payout #2 to recipient1 ($50).
- Verify: Both complete successfully.
- Check balance: $100 total received.
- Execute payout to recipient1 ($50).
- Simultaneously execute payout to recipient2 ($50).
- Verify: Both complete successfully.
- Start payout process.
- Disable network mid-transaction.
- Verify: Error handling activates.
- Check: Transaction not partially completed.
Use this checklist to ensure comprehensive testing:
- 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
- Network errors
- API errors
- Validation errors
- User-facing error messages
- Error logging
- Backend API calls
- Session creation
- Webhook delivery (if implemented)
- Webhook signature validation
- Button rendering
- Loading states
- Success messages
- Error messages
- Cancellation flow
- Retry options
Once all tests pass in sandbox:
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'
}
};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
}
};Before full rollout:
- Execute test payout of $1.00 to your own PayPal account.
- Verify funds received.
- Verify transaction appears in PayPal reports.
- Test refund flow if applicable.
- Set up error tracking (e.g., Sentry, Datadog).
- Configure alerts for payout failures.
- Monitor webhook delivery rates.
- Track payout completion times.
- Beta users: Release to small group (5-10%).
- Monitor: Watch for errors and user feedback.
- Iterate: Fix any issues discovered.
- Expand: Gradually increase rollout percentage.
- Full release: Roll out to all users.
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.