Last updated: October 14, 2026
Checkout starts from a PayPal button. Your server creates and captures the order, and the SDK takes the buyer through approval in the PayPal app or a browser.
The buyer approves with Face ID, Touch ID, or a passkey, then returns to your app through your Universal Link. If the buyer hasn't installed the PayPal app or the buyer is not eligible, checkout continues in an in-app browser automatically.
When the buyer taps the PayPal button, call createPayPalSession() and create the order at the same time. Then call start(orderID:completion:) with the order ID.
The start() method requires a prepared shopper session. If you call it before createPayPalSession(), you receive PayPalError.sessionNotStartedError.
Call createPayPalSession() from your button's action. It sends the session type, buyer identity, return URLs, and user action to PayPal. Then it determines whether checkout opens in the PayPal app or an in-app browser. Create the PayPal session and order at the same time so both requests can 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. Your server creates and captures orders 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 you must call createPayPalSession() before start().
Complete Install and set up. Then, for PayPal Checkout:
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. Pass return and cancel URLs to the SDK through the URL config from install and set up.
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. When the last step completes, your server has captured the order and the buyer sees confirmation of their payment.
Render a PayPalButton on your checkout screen with an action. It needs no order or session to display.
Disable the button as soon as the buyer taps it, and show a loading indicator while createPayPalSession(), order creation, and start() are in flight. This prevents duplicate submissions.
// 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: "buyer@example.com", phone: nil), // optional: omit or pass nil when you have no hint
urlConfig: urlConfig,
userAction: .continue // .payNow for a "Pay Now" button
)Help reduce declines. Collect device data before you create the order, and attach the resulting risk data to your create-order request.
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 completion you passed to start() delivers the result.
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let url = userActivity.webpageURL else { return }
checkoutClient.handleReturnURL(url)
}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 checkout.
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.
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. The userIdentity parameter is optional. Omit it or pass nil when you have no buyer hint. PayPal still creates the session. When you have them, pass both email and phone: together they improve app-switch eligibility, risk assessment, and approval rates beyond email alone.
| Option | When to use |
|---|---|
PayPalUserIdentity(email:phone:) | You know the buyer's email, phone number, or both. 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 during a purchase with a single approval. The button sequence is the same as for a regular checkout.
The only difference is that your server creates the order with payment_source.paypal.attributes.vault set. When saving a payment method, 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 carries only orderID and payerID. Retrieve the vaulted token server-side after capture, either by getting the order or through the Payment Method Tokens API.
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. Treat cancellation as a normal buyer choice.
.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.Test on a physical device, because the app-switch path requires one.
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 | Your app returns the buyer to your checkout screen and PayPal makes no charge. |
| Save with purchase | Order captures. The vaulted token is retrievable server-side. |
| 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, and that it returns the buyer to your app.