# Accept a PayPal payment (/sdk/ios/add-payment-methods/paypal-checkout)

Accept a PayPal payment in your iOS app with PayPal Mobile SDK v3.0.0, including one-time checkout, save with purchase, and Pay Later and PayPal Credit.



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.

> **Info:** Complete [Install and set up](/sdk/ios/install-and-setup).

## Overview [#overview]

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.

```mermaid
sequenceDiagram
    participant App as Your App
    participant SDK as PayPal SDK
    participant PayPal as PayPal (app or browser)
    participant Server as Your Server

    Note over App: Buyer taps your PayPal button
    par Prepare the session
        App->>SDK: createPayPalSession(sessionType: .checkout, userIdentity, urlConfig, userAction)
    and Create the order
        App->>Server: Create order (Orders v2)
        Server-->>App: orderID
    end
    App->>SDK: start(orderID, completion)
    alt Buyer eligible and PayPal app installed
        SDK->>PayPal: Open the PayPal app through your Universal Link
        Note over PayPal: Buyer approves with Face ID, Touch ID, or a passkey
    else App not installed or buyer not eligible
        SDK->>PayPal: Open checkout in ASWebAuthenticationSession
        Note over PayPal: Buyer logs in and approves
    end
    PayPal-->>App: Return to your app (Universal Link)
    App->>SDK: handleReturnURL(url)
    SDK->>App: .success(orderID, payerID) / .failure(checkoutCanceledError) / .failure(error)
    App->>Server: Capture order (Orders v2)
```

## How the SDK works [#how-the-sdk-works]

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:

* Configure the client.
* In your button's action, call `createPayPalSession()`, create the order, and pass its ID to `start()`.
* Forward the return URL to the SDK and capture.

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

## Before you begin [#before-you-begin]

Complete Install and set up. For PayPal Checkout specifically:

* Add the `PayPalPayments` and `PaymentButtons` products.
* Construct the client from the `CoreConfig` you built in setup:

```swift
let checkoutClient = PayPalClient(config: config)   // config + urlConfig from Install and set up
```

## Server: create an order [#server-create-an-order]

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

```shell
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" } } ] }'
```

## Integrate PayPal Checkout [#integrate-paypal-checkout]

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.

### 1. Add the PayPal button [#1-add-the-paypal-button]

Render a `PayPalButton` on your checkout screen with an action. It needs no order or session to display.

```swift
// SwiftUI
PayPalButton.Representable(color: .gold, size: .collapsed) {
    beginCheckout()   // Steps 2-4
}
```

### 2. Create the PayPal session [#2-create-the-paypal-session]

Call this in the button's action, before or alongside order creation. It returns immediately and prepares the session in the background.

```swift
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
)
```

### 3. Collect device data [#3-collect-device-data]

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.

```swift
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.
```

### 4. Create the order and start checkout [#4-create-the-order-and-start-checkout]

Create the order on your server, then hand the order ID to the SDK to launch the app-switch or in-app browser flow.

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

### 5. Handle the return [#5-handle-the-return]

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

```swift
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard let url = userActivity.webpageURL else { return }
    checkoutClient.handleReturnURL(url)
}
```

### 6. Capture the order [#6-capture-the-order]

With the buyer's approval confirmed, capture the order on your server to complete the payment.

```swift
func captureOrder(_ orderID: String) {
    // POST /v2/checkout/orders/{orderID}/capture on your server
    myServer.captureOrder(orderID)
}
```

## Identity: PayPalUserIdentity [#identity-paypaluseridentity]

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 [#save-with-purchase]

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:

```shell
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 without purchase [#save-without-purchase]

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

```swift
// 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 [#pay-later-and-paypal-credit]

Pay Later and PayPal Credit are PayPal funding sources with dedicated buttons:

* `PayPalPayLaterButton`
* `PayPalCreditButton`

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

```swift
// SwiftUI
PayPalPayLaterButton.Representable(color: .gold, size: .collapsed) {
    beginCheckout()   // same createPayPalSession() -> start(orderID:completion:) flow as Step 2-4 above
}
```

## Handle results [#handle-results]

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** (`.success`): checkout or save with purchase, capture the order. Save without purchase, store the returned `tokenID` and `approvalSessionID`.
* **Cancel** (`.failure(error)`, where `isCheckoutCanceled(error)` / `isVaultCanceled(error)` is `true`): return the buyer to your checkout or save screen. No charge was made.
* **Failure** (`.failure(error)`, any other error): show an error. `sessionNotStartedError` means `createPayPalSession()` was not called before `start()` / `vault()`. Fix the ordering.

## Best practices [#best-practices]

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 and go live [#test-and-go-live]

Test on a physical device. The app-switch path does not work on the simulator.

### Trigger the app-switch path [#trigger-the-app-switch-path]

The SDK switches to the PayPal app only when all of the following hold. Otherwise it falls back to `ASWebAuthenticationSession`:

* A physical device with the PayPal app installed, using the sandbox app for sandbox testing.
* Merchant and buyer are in the US, and your integration is App Switch eligible.
* The buyer-identity email you pass to `createPayPalSession()` matches the account signed into the PayPal app.
* In the PayPal app, **Extend your login session** and Face ID / Touch ID are enabled under **Avatar › Login and security**.

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](https://developer.paypal.com/braintree/docs/guides/paypal/testing-go-live/ios/v6#testing-app-switch).

### Test scenarios [#test-scenarios]

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

### Go live checklist [#go-live-checklist]

* Call `createPayPalSession()` on the button tap, before or alongside order creation.
* Switch `.sandbox` to `.live` and use your live client ID and merchant ID.
* Verify the Universal Link return works in a release build, with AASA hosted and verified.
* Verify the custom-scheme fallback, `fallbackSchemeURL`, is registered and returns the buyer to your app.
* Confirm success, cancellation, and failure are all handled.
* Collect device data and pass the client metadata ID on your Orders v2 request.
* Contact your PayPal account team to enable App Switch for production traffic.

## See also [#see-also]

* [Buttons](/sdk/ios/add-payment-methods/payment-buttons): add and style the PayPal button that starts this flow.
* [Troubleshooting](/sdk/ios/troubleshooting): diagnose common build and integration failures by symptom.
* [PayPal Developer Dashboard](/dashboard/): get your client ID and merchant ID and enable payment methods.
* [Orders v2 API](/api/orders/v2/): create, authorize, capture, and refund orders from your server.
