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

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



Accept a PayPal payment in your Android app with PayPal Mobile SDK v3.0.0. 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 biometrics or a passkey, then returns to your app through your Android App Link. If the PayPal app is not installed or the buyer is not eligible, checkout continues in a Chrome Custom Tab automatically.

> **Info:** Complete [Install and set up](/sdk/android/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(activity, orderId, callback)` with the order ID.

`start()` requires a prepared shopper session. If you call it before `createPayPalSession()`, it fails with `PayPalError.sessionNotCreatedError` and logs the `SESSION_NOT_STARTED` event.

Call `createPayPalSession()` from your button’s `onClick` handler. It sends the token 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 `start()` callback only reports whether the authentication challenge launched. To receive the buyer’s approval, cancellation, or failure result, forward the return intent to `finishStart()`.

```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(tokenType=ORDER_ID, userIdentity, urlConfig, userAction)
    and Create the order
        App->>Server: Create order (Orders v2)
        Server-->>App: orderId
    end
    App->>SDK: start(activity, orderId, callback)
    alt Buyer eligible and PayPal app installed
        SDK->>PayPal: Open the PayPal app through your App Link
        Note over PayPal: Buyer approves with biometrics or a passkey
    else App not installed or buyer not eligible
        SDK->>PayPal: Open checkout in a Chrome Custom Tab
        Note over PayPal: Buyer logs in and approves
    end
    SDK->>App: PayPalPresentAuthChallengeResult (challenge presented)
    PayPal-->>App: Return to your app (App Link)
    App->>SDK: finishStart(intent)
    SDK->>App: PayPalFinishStartResult: Success / Canceled / Failure / NoResult
    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 `onClick` handler, call `createPayPalSession()`, create the order, and pass the order ID and your `Activity` to `start()`.
* Forward the return intent to `finishStart()` 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 a PayPal Checkout integration:

* Add the `com.paypal.android:paypal-payments` and `com.paypal.android:payment-buttons` modules.
* Construct the client from the `CoreConfig` you built in setup:

```kotlin
val checkoutClient = PayPalClient(context, config)   // config 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 and set a click listener. It needs no order or session to display. Declare it in your layout, or create it programmatically, then wire the tap:

```kotlin
payPalButton.setOnClickListener {
    beginCheckout()   // Steps 2-4
}
```

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

Call this in the button's `onClick`, before or alongside order creation. It returns immediately and prepares the session in the background. Pass `TokenType.ORDER_ID` for One-Time Checkout and save with purchase. Both are approved against an order ID; see [Save without purchase](#save-without-purchase) for the vault-only case.

```kotlin
import com.paypal.android.corepayments.model.TokenType

checkoutClient.createPayPalSession(
    tokenType    = TokenType.ORDER_ID,
    userIdentity = PayPalUserIdentity(email = "buyer@example.com"),  // or null, see Identity below
    urlConfig    = urlConfig,
    userAction   = PayPalUserAction.CONTINUE   // PAY_NOW 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 client metadata ID to your create-order request so PayPal's risk systems can reduce declines.

```kotlin
val dataCollector = PayPalDataCollector(config)
val clientMetadataId = dataCollector.collectDeviceData(
    context, PayPalDataCollectorRequest(hasUserLocationConsent = false)
)
// Send clientMetadataId to your server; 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.

```kotlin
val orderId = myServer.createOrder()   // include the client metadata ID from Step 3

checkoutClient.start(
    activity = this,
    orderId  = orderId,
    callback = object : PayPalResultCallback {
        override fun onPayPalResult(result: PayPalPresentAuthChallengeResult) {
            when (result) {
                is PayPalPresentAuthChallengeResult.Success -> { /* Challenge presented (app switch or browser), awaiting the buyer's return */ }
                is PayPalPresentAuthChallengeResult.Failure -> showError(result.error)   // includes SESSION_NOT_STARTED
            }
        }
    }
)
```

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

Forward the return intent to `finishStart()` when your activity re-enters the foreground. This is where the buyer's actual approval, cancellation, or failure is delivered. `start()`'s callback only confirmed the challenge launched.

```kotlin
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    when (val result = checkoutClient.finishStart(intent)) {
        is PayPalFinishStartResult.Success  -> result.orderId?.let { captureOrder(it) }   // result.payerId also available; both are nullable
        is PayPalFinishStartResult.Canceled -> showCheckoutScreen()   // result.orderId also available, if present
        is PayPalFinishStartResult.Failure  -> showError(result.error)   // result.orderId also available, if present
        PayPalFinishStartResult.NoResult    -> Unit   // unrelated intent; ignore
        null                                 -> Unit   // finishStart() called without a matching start()/vault() call in this process
    }
}
```

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

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

```kotlin
fun captureOrder(orderId: String) {
    // POST /v2/checkout/orders/{orderId}/capture on your server
    myServer.captureOrder(orderId)
}
```

## Identity: PayPalUserIdentity [#identity-paypaluseridentity]

You pass `userIdentity` to `createPayPalSession()` as a `PayPalUserIdentity` data class. The hint helps PayPal recognize the buyer, which can increase app-switch eligibility and approval rates. The SDK sends email and phone to PayPal for matching. `userIdentity` is nullable. Pass `null` when you have no buyer hint. The session is still created.

| Field                     | Type                 | When to use                                                                           |
| ------------------------- | -------------------- | ------------------------------------------------------------------------------------- |
| `existingPayPalSessionId` | `String?`            | Your server already created a buyer session. Pass its ID.                             |
| `email`                   | `String?`            | You know the buyer's email address.                                                   |
| `phone`                   | `PayPalPhoneNumber?` | You know the buyer's phone number. Construct with `countryCode` and `nationalNumber`. |

```kotlin
// Email only
PayPalUserIdentity(email = "buyer@example.com")

// Email and phone together improve matching further
PayPalUserIdentity(
    email = "buyer@example.com",
    phone = PayPalPhoneNumber(countryCode = "1", nationalNumber = "4085551234")
)

// Server-side shopper session
PayPalUserIdentity(existingPayPalSessionId = serverSideSessionId)

// No buyer hint
checkoutClient.createPayPalSession(tokenType = TokenType.ORDER_ID, userIdentity = null, urlConfig = urlConfig, userAction = PayPalUserAction.CONTINUE)
```

## 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(tokenType = TokenType.ORDER_ID, ...)` → `start(activity, orderId, callback)` → `finishStart(intent)` sequence described in [Integrate PayPal Checkout](#integrate-paypal-checkout).

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 App 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 `PayPalFinishStartResult.Success` still only carries the order ID and payer ID. The SDK does not return a payment token for this flow. Retrieve the vaulted token server-side after capture, either by getting the order or through the Payment Method Tokens API.

## Save without purchase [#save-without-purchase]

Save a buyer's PayPal account for future charges with no purchase now. Call `createPayPalSession()` first with `tokenType = TokenType.VAULT_ID`, using `SETUP_NOW` for a "Set up now" button, then create the setup token on your server and call `vault()`.

```kotlin
// In your "Set up now" button's onClick handler:
checkoutClient.createPayPalSession(
    tokenType    = TokenType.VAULT_ID,
    userIdentity = userIdentity,
    urlConfig    = urlConfig,
    userAction   = PayPalUserAction.SETUP_NOW
)

val setupTokenId = myServer.createSetupToken()

checkoutClient.vault(
    activity     = this,
    setupTokenId = setupTokenId,
    callback = object : PayPalResultCallback {
        override fun onPayPalResult(result: PayPalPresentAuthChallengeResult) {
            when (result) {
                is PayPalPresentAuthChallengeResult.Success -> { /* Challenge presented, awaiting the buyer's return */ }
                is PayPalPresentAuthChallengeResult.Failure -> showError(result.error)   // includes SESSION_NOT_STARTED
            }
        }
    }
)
```

Forward the return intent to `finishVault()` in `onNewIntent`, the same way checkout forwards to `finishStart()`. The vaulted approval is identified by `approvalSessionId`, not the setup token ID:

```kotlin
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    when (val result = checkoutClient.finishVault(intent)) {
        is PayPalFinishVaultResult.Success -> savePaymentToken(result.approvalSessionId)
        PayPalFinishVaultResult.Canceled   -> showVaultScreen()
        is PayPalFinishVaultResult.Failure -> showError(result.error)
        PayPalFinishVaultResult.NoResult   -> Unit   // unrelated intent; ignore
        null                                -> Unit   // finishVault() called without a matching start()/vault() call in this process
    }
}
```

## Pay Later and PayPal Credit [#pay-later-and-paypal-credit]

Pay Later and PayPal Credit are PayPal funding sources, not separate SDK flows. Select the funding source on your server when you create the order by setting `payment_source.paypal.experience_context.payment_method_selected` to one of these values:

* `PAYPAL` (default)
* `PAYPAL_PAY_LATER`
* `PAYPAL_CREDIT`

The client-side flow is the same for all three:

`createPayPalSession()` → `start(activity, orderId, callback)` → `finishStart(intent)`

For details, see [**Integrate PayPal Checkout**](/sdk/android/add-payment-methods/paypal-checkout#integrate-paypal-checkout).

Pay Later and PayPal Credit support One-Time Checkout and save with purchase. They don't support save without purchase.

Use `PayPalCheckoutFundingSource` (`PAYPAL`, `PAY_LATER`, or `PAYPAL_CREDIT`) in your app code to track the buyer's selection and map it to the corresponding `payment_method_selected` value in your create-order request. The SDK doesn't pass this enum to any `PayPalClient` method.

You can also add dedicated buttons with the `payment-buttons` module:

* `PayLaterButton`
* `PayPalCreditButton`

## Handle results [#handle-results]

`start()` and `vault()` deliver a `PayPalPresentAuthChallengeResult` that only confirms whether the auth challenge, app switch or Custom Tab, launched. The buyer's actual outcome arrives later, when you forward the return intent to `finishStart()` for checkout, including save with purchase, or `finishVault()` for save without purchase. Handle all cases. Cancellation is a normal buyer choice, not an error.

**`start()` and `vault()` callback** returns a `PayPalPresentAuthChallengeResult`:

* `Success`: only confirms the challenge launched.
* `Failure(error)`: checkout or vault never launched. Show the error. `SESSION_NOT_STARTED` means `createPayPalSession()` was not called before `start()` or `vault()`. Fix the ordering.

**`finishStart(intent)`** returns a `PayPalFinishStartResult?`:

* `Success(orderId?, payerId?)`: capture the order if `orderId` is present.
* `Canceled(orderId?)`: return the buyer to your checkout screen. `orderId` is included when available, and no charge was made.
* `Failure(error, orderId?)`: show an error. `orderId` is included when available.
* `NoResult`: the intent was not a PayPal return. Ignore it.
* `null`: `finishStart()` was called with no matching `start()` or `vault()` call in this process. Ignore it.

**`finishVault(intent)`** returns a `PayPalFinishVaultResult?`:

* `Success(approvalSessionId)`: store the returned `approvalSessionId`.
* `Canceled`: return the buyer to your save screen.
* `Failure(error)`: show an error.
* `NoResult`: the intent was not a PayPal return. Ignore it.
* `null`: same as `finishStart()`'s `null` case. No matching `start()` or `vault()` call in this process.

## 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, in `onNewIntent` or `onResume`. 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 a Chrome tab instead of your app, usually because the App Link is not verified. Registering a `fallbackSchemeUrl` alongside your App Link 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 an emulator.

### 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 a Chrome Custom Tab:

* 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 fingerprint 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 Android testing guide](https://developer.paypal.com/braintree/docs/guides/paypal/testing-go-live/android/v5#testing-app-switch).

### Test scenarios [#test-scenarios]

| Scenario                                   | Expected result                                                                                                                   |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| App switch: end to end                     | The buyer switches to the PayPal app, approves, 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 a Chrome Custom Tab 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 approval session ID from `finishVault()`, and can charge the vaulted account later.                     |
| Pay Later and 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 `CoreEnvironment.SANDBOX` to `CoreEnvironment.LIVE` and use your live client ID and merchant ID.
* Verify the App Link return works in a release build, with assetlinks.json hosted and verified.
* Verify the custom-scheme fallback, `fallbackSchemeUrl`, is registered and returns the buyer to your app.
* Confirm all result variants are handled: `PayPalPresentAuthChallengeResult` (Success, Failure) from `start()` or `vault()`, and `PayPalFinishStartResult` and `PayPalFinishVaultResult` (Success, Canceled, Failure, NoResult, and `null`) from `finishStart()` or `finishVault()`.
* 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/android/add-payment-methods/payment-buttons): add and style the PayPal button that starts this flow.
* [Troubleshooting](/sdk/android/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.
