On this page
No Headings
Last updated: September 14, 2026
This guide shows you how to accept a PayPal payment in your iOS app with PayPal Mobile SDK v3.0.0: One-Time Checkout, save payment methods (with or without a purchase), and Pay Later / PayPal Credit. The PayPal button is the highlighted way to start checkout: when the buyer taps it, checkout happens in the PayPal app if they are eligible and have it installed, approving with Face ID, Touch ID, or a passkey, then returns to your app through your Universal Link. If the PayPal app is not installed or the buyer is not eligible, checkout continues in an in-app browser automatically.
Before you start
Complete Install and set up.
When the buyer taps your PayPal button, call createPayPalSession() and create the order at the same time. Then call start(orderID:completion:) with the order ID.
start() requires a prepared shopper session. If you call it before createPayPalSession(), its completion receives PayPalError.sessionNotStartedError.
Call createPayPalSession() from your button's action. It sends the session type, buyer identity, return URLs, and user action to PayPal and determines whether checkout opens in the PayPal app or an in-app browser. Calling it while you create the order allows both network requests to run in parallel.
The completion you pass to start() isn't called until the buyer returns and you forward the URL to handleReturnURL(). That call delivers the buyer's actual success, cancellation, or failure result.
The SDK handles the client side of checkout. It does not create or capture orders. Your server does that with the Orders v2 API. Your responsibilities are:
createPayPalSession(), create the order, and pass its ID to start().The SDK chooses the checkout experience, either the PayPal app or the in-app browser, and returns the buyer to your app. The one ordering rule is that createPayPalSession() must be called before start().
Complete Install and set up. For PayPal Checkout specifically:
PayPalPayments and PaymentButtons products.CoreConfig you built in setup:let checkoutClient = PayPalClient(config: config) // config + urlConfig from Install and set upCall the Orders v2 API server-to-server and return only the order ID to your app. Return and cancel URLs are passed to the SDK through the URL config from Install and set up, not in the Orders API body.
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{ "intent": "CAPTURE", "purchase_units": [ { "amount": { "currency_code": "USD", "value": "49.99" } } ] }'These steps take the buyer from tapping the PayPal button to a captured payment: render the button, prepare the session, create the order, and handle the buyer's return. When the last step completes, the order is captured and the buyer sees their payment confirmed.
Render a PayPalButton on your checkout screen with an action. It needs no order or session to display.
// SwiftUI
PayPalButton.Representable(color: .gold, size: .collapsed) {
beginCheckout() // Steps 2-4
}Call this in the button's action, before or alongside order creation. It returns immediately and prepares the session in the background.
checkoutClient.createPayPalSession(
sessionType: .checkout,
userIdentity: PayPalUserIdentity(email: "[email protected]", phone: nil), // optional: omit or pass nil when you have no hint
urlConfig: urlConfig,
userAction: .continue // .payNow for a "Pay Now" button
)Collect device data before you create the order, and attach the resulting risk data to your create-order request so PayPal's risk systems can reduce declines.
let dataCollector = PayPalDataCollector(config: config)
let riskCorrelationPayload = dataCollector.collectDeviceData()
// collectDeviceData() returns a JSON string, for example {"correlation_id":"..."}, not a bare ID.
// Send it to your server, and set it as the PayPal-Client-Metadata-Id header on your Orders v2 create call.Create the order on your server, then hand the order ID to the SDK to launch the app-switch or in-app browser flow.
let orderID = try await myServer.createOrder() // include the client metadata ID from Step 3
checkoutClient.start(orderID: orderID) { result in
switch result {
case .success(let checkout):
captureOrder(checkout.orderID)
case .failure(let error) where PayPalError.isCheckoutCanceled(error):
showCheckoutScreen()
case .failure(let error):
showError(error.localizedDescription) // includes sessionNotStarted
}
}Forward the return URL to the SDK from your scene continuation, or .onOpenURL in SwiftUI. The result is delivered through the completion you passed to start().
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let url = userActivity.webpageURL else { return }
checkoutClient.handleReturnURL(url)
}With the buyer's approval confirmed, capture the order on your server to complete the payment.
func captureOrder(_ orderID: String) {
// POST /v2/checkout/orders/{orderID}/capture on your server
myServer.captureOrder(orderID)
}You pass userIdentity to createPayPalSession(). The hint helps PayPal recognize the buyer, which can increase app-switch eligibility and approval rates. The SDK hashes email and phone using SHA-256 before sending. userIdentity is optional. Omit it or pass nil when you have no buyer hint. The session is still created.
| Option | When to use |
|---|---|
PayPalUserIdentity(email:phone:) | You know the buyer's email and/or phone. phone takes a PayPalPhoneNumber(countryCode:nationalNumber:). |
PayPalUserIdentity(existingPayPalSessionID:) | Your server already created a PayPal shopper session. Pass its ID. |
nil | You have no buyer hint. The session is still created. |
Save with purchase saves the buyer's PayPal account while completing a real purchase, in a single approval. There is no separate client call. Use the exact same button → createPayPalSession() → start(orderID:) sequence above.
The only difference is that your server creates the order with payment_source.paypal.attributes.vault set. When vaulting, the API also requires experience_context.return_url and cancel_url on the request — the SDK's Universal Link handles the buyer's actual return, so these can be placeholder values:
curl -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{
"intent": "CAPTURE",
"purchase_units": [ { "amount": { "currency_code": "USD", "value": "49.99" } } ],
"payment_source": {
"paypal": {
"experience_context": { "return_url": "https://example.com/return", "cancel_url": "https://example.com/cancel" },
"attributes": { "vault": { "store_in_vault": "ON_SUCCESS", "usage_type": "MERCHANT", "customer_type": "CONSUMER" } }
}
}
}'On approval PayPal both completes the purchase and stores the payment method, but the success result still only carries orderID and payerID. The SDK does not return a payment token for this flow. Retrieve the vaulted token server-side, either by GETting the order or through the Payment Method Tokens API, after capture.
Save a buyer's PayPal account for future charges with no purchase now. Call createPayPalSession() first (use sessionType: .vaultWithoutPurchase and userAction: .setupNow for a "Set up now" button), then create the setup token on your server and call vault(setupTokenID:completion:).
// In your "Set up now" button's action:
checkoutClient.createPayPalSession(
sessionType: .vaultWithoutPurchase,
userIdentity: userIdentity,
urlConfig: urlConfig,
userAction: .setupNow
)
let setupTokenID = try await myServer.createSetupToken()
checkoutClient.vault(setupTokenID: setupTokenID) { result in
switch result {
case .success(let vault):
savePaymentToken(vault.tokenID)
case .failure(let error) where PayPalError.isVaultCanceled(error):
showVaultScreen()
case .failure(let error):
showError(error.localizedDescription) // includes sessionNotStarted
}
}Pay Later and PayPal Credit are PayPal funding sources with dedicated buttons:
PayPalPayLaterButtonPayPalCreditButtonBoth buttons are part of PaymentButtons. Connect each button's action to the same createPayPalSession() → start(orderID:completion:) sequence used for One-Time Checkout.
Eligible buyers automatically see Pay Later or PayPal Credit financing offers during approval.
Both funding sources support One-Time Checkout and save with purchase. They don't support save without purchase.
// SwiftUI
PayPalPayLaterButton.Representable(color: .gold, size: .collapsed) {
beginCheckout() // same createPayPalSession() -> start(orderID:completion:) flow as Step 2-4 above
}Checkout (including save with purchase) and save without purchase each deliver a Result<_, CoreSDKError> with two cases: .success and .failure. Cancellation is delivered as a .failure with a specific error, not a separate case. Check for it with PayPalError.isCheckoutCanceled(_:) for checkout and save with purchase, or PayPalError.isVaultCanceled(_:) for save without purchase. Handle both outcomes. Cancellation is a normal buyer choice, not a failure to surface as an error.
.success): checkout or save with purchase, capture the order. Save without purchase, store the returned tokenID and approvalSessionID..failure(error), where isCheckoutCanceled(error) / isVaultCanceled(error) is true): return the buyer to your checkout or save screen. No charge was made..failure(error), any other error): show an error. sessionNotStartedError means createPayPalSession() was not called before start() / vault(). Fix the ordering.Show a loading indicator after the button tap. Disable the button immediately after the buyer taps it, and show a loading indicator while createPayPalSession(), order creation, and start() are in flight. This prevents duplicate submissions.
Provide buyer email and phone. Pass both in userIdentity when you have them. Together they improve app-switch eligibility, risk assessment, and approval rates beyond email alone.
Handle the return to your app. Remove the loading indicator as soon as your app returns to the foreground. If the buyer backgrounded the PayPal app without approving or canceling, let them resume rather than restarting checkout.
Handle redirection to the browser. Occasionally the OS opens the return in the default mobile browser instead of your app, usually because the Universal Link is not correctly associated with your domain. Setting fallbackSchemeURL covers the common case. As a safeguard, consider website logic that detects a redirected buyer, confirms order status, and guides them back.
Test on a physical device. The app-switch path does not work on the simulator.
The SDK switches to the PayPal app only when all of the following hold. Otherwise it falls back to ASWebAuthenticationSession:
createPayPalSession() matches the account signed into the PayPal app.To test the in-app browser path, use a device without the PayPal app installed, or an ineligible buyer. Full sandbox-app setup lives in the published PayPal iOS testing guide.
| Scenario | Expected result |
|---|---|
| App switch: end to end | The buyer switches to the PayPal app, approves with Face ID / Touch ID or a passkey, returns to your app, and the order captures. |
| In-app browser: end to end | With the PayPal app not installed, or with an ineligible buyer, checkout completes in ASWebAuthenticationSession and the order captures. |
| Buyer cancels in PayPal | The buyer is returned to your checkout screen and no charge is made. |
| Save with purchase | Order captures. The vaulted token is retrievable server-side, not from the SDK result. |
| Save without purchase | You receive and store the tokenID / approvalSessionID, and can charge it later. |
| Pay Later / PayPal Credit eligible buyer | Financing offers render. Approval and capture proceed the same as standard PayPal. |
createPayPalSession() on the button tap, before or alongside order creation..sandbox to .live and use your live client ID and merchant ID.fallbackSchemeURL, is registered and returns the buyer to your app.