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 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.
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.
The start() method 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. Create the PayPal session and order at the same time so both requests can 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().
The SDK handles the client side of checkout. Your server creates and captures orders with the Orders v2 API. Your responsibilities are:
onClick handler, call createPayPalSession(), create the order, and pass the order ID and your Activity to start().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 you must call createPayPalSession() before start().
Complete Install and set up. For a PayPal Checkout integration:
com.paypal.android:paypal-payments and com.paypal.android:payment-buttons modules.CoreConfig you built in setup:val checkoutClient = PayPalClient(context, config) // config 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 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:
payPalButton.setOnClickListener {
beginCheckout() // Steps 2-4
}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.
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. PayPal approves both against an order ID. See Save without purchase for the vault-only case.
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
)Help reduce declines. Collect device data before you create the order, and attach the resulting client metadata ID to your create-order request.
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.Create the order on your server, then hand the order ID to the SDK to launch the app-switch or in-app browser flow.
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
}
}
}
)When your activity re-enters the foreground, forward the return intent to finishStart(). This intent delivers the buyer’s approval, cancellation, or failure. The start() callback only confirms that the challenge launched.
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
}
}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 checkout.
Occasionally the OS opens the return in a Chrome tab instead of your app, usually because Android has not verified the App Link. 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.
With the buyer's approval confirmed, capture the order on your server to complete the payment.
fun captureOrder(orderId: String) {
// POST /v2/checkout/orders/{orderId}/capture on your server
myServer.captureOrder(orderId)
}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. The userIdentity parameter is nullable. Pass null 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.
| 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. |
// 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 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 App 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 PayPalFinishStartResult.Success carries only the order ID and payer ID. 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 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().
// 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 approvalSessionId value identifies the vaulted approval.
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 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_LATERPAYPAL_CREDITAll three funding sources follow the same client-side flow as PayPal Checkout:
createPayPalSession() → start(activity, orderId, callback) → finishStart(intent)
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:
PayLaterButtonPayPalCreditButtonThe SDK reports results in two stages:
start() or vault() callback confirms whether the authentication challenge launched.finishStart() or finishVault() method reports the buyer’s final outcome.Handle every result described below. Treat cancellation as a normal buyer choice.
The callbacks for start() and vault() return a PayPalPresentAuthChallengeResult. Handle these results:
Failure(error)): Checkout or vaulting did not launch. Show the error.The SESSION_NOT_STARTED event means your app called start() or vault() before createPayPalSession(). Call createPayPalSession() first.
Forward the return intent to finishStart() for checkout and save with purchase. This method returns a PayPalFinishStartResult?.
Handle these results:
Success(orderId?, payerId?)): Capture the order if orderId is present.Canceled(orderId?)): Return the buyer to your checkout screen. The result includes orderId when available. No charge occurs.Failure(error, orderId?)): Show the error. The result includes orderId when available.NoResult): The intent was not a PayPal return. Ignore it.null): No matching start() or vault() call occurred in this process. Ignore the result.Forward the return intent to finishVault() for save without purchase. This method returns a PayPalFinishVaultResult?.
Handle these results:
Success(approvalSessionId)): Store the returned approvalSessionId.Canceled): Return the buyer to your save screen.Failure(error)): Show the error.NoResult): The intent was not a PayPal return. Ignore it.null): No matching start() or vault() call occurred in this process. Ignore the result.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 a Chrome Custom Tab:
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 Android testing guide.
| 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 | 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 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. |
createPayPalSession() on the button tap, before or alongside order creation.CoreEnvironment.SANDBOX to CoreEnvironment.LIVE and use your live client ID and merchant ID.fallbackSchemeUrl, and that it returns the buyer to your app.PayPalPresentAuthChallengeResult (Success, Failure) from start() or vault(), and PayPalFinishStartResult and PayPalFinishVaultResult (Success, Canceled, Failure, NoResult, and null) from finishStart() or finishVault().