On this page
No Headings
Last updated: September 14, 2026
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.
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(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().
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:
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 createPayPalSession() must be called 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. 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 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
}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 for the vault-only case.
import com.paypal.android.corepayments.model.TokenType
checkoutClient.createPayPalSession(
tokenType = TokenType.ORDER_ID,
userIdentity = PayPalUserIdentity(email = "[email protected]"), // or null, see Identity below
urlConfig = urlConfig,
userAction = PayPalUserAction.CONTINUE // PAY_NOW for a "Pay Now" button
)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.
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
}
}
}
)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.
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
}
}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. 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. |
// Email only
PayPalUserIdentity(email = "[email protected]")
// Email and phone together improve matching further
PayPalUserIdentity(
email = "[email protected]",
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 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.
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:
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 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 vaulted approval is identified by approvalSessionId, not the setup token ID:
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_CREDITThe client-side flow is the same for all three:
createPayPalSession() → start(activity, orderId, callback) → finishStart(intent)
For details, see 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:
PayLaterButtonPayPalCreditButtonstart() 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.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 on a physical device. The app-switch path does not work on an emulator.
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 | 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. |
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, is registered and 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().