> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Android

> Open Dodo Payments hosted checkout from an Android app in a Custom Tab with the official Android SDK, and get a typed checkout result back from one call.

<Info>
  This page covers the Android checkout SDK, `com.dodopayments.api:checkout-android`, which opens Dodo Payments hosted checkout inside your app. To call the Dodo Payments API from your server, use the [backend Kotlin SDK](/developer-resources/sdks/kotlin) instead.
</Info>

<CardGroup cols={2}>
  <Card title="Checkout Sessions API" icon="cart-shopping" href="/developer-resources/checkout-session">
    Create the `checkout_url` that this SDK opens.
  </Card>

  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    Best practices for mobile checkout flows.
  </Card>
</CardGroup>

The Android SDK opens Dodo Payments hosted checkout in a Custom Tab (`androidx.browser.customtabs`) and returns a typed `CheckoutResult` when the customer finishes or leaves checkout. Your backend creates the checkout session and sends its `checkout_url` to the app. The SDK contains no networking code and holds no API key, so it never calls the Dodo Payments API.

**Requirements:** `minSdk` 23, Kotlin, and Java 17. The SDK depends only on `androidx.activity`, `androidx.browser`, and `kotlinx-coroutines-android`.

## Installation

<Steps>
  <Step title="Add the Dependency">
    Add the SDK from Maven Central to your app module's `build.gradle.kts`:

    ```kotlin build.gradle.kts theme={null}
    dependencies {
        implementation("com.dodopayments.api:checkout-android:1.1.0")
    }
    ```

    [Appearance customization](#appearance-customization) requires version 1.1.0 or later.
  </Step>

  <Step title="Register a Callback URL Scheme">
    Set your callback scheme as a Gradle manifest placeholder. The SDK's own manifest declares the redirect activity's intent filter with the `${dodoCallbackScheme}` placeholder, so this property is the only setup step. You don't add any manifest XML:

    ```kotlin build.gradle.kts theme={null}
    android {
        defaultConfig {
            manifestPlaceholders["dodoCallbackScheme"] = "myapp"
        }
    }
    ```

    Use the same scheme in `CheckoutParams.returnUrl`, for example `myapp://checkout/return`, and set the same URL as the checkout session's `return_url` when your backend creates the session. The SDK matches the return URL on scheme, host, and path, and ignores the query string. The URL doesn't need to load a real page.

    <Note>
      If you omit the placeholder, the build fails with an unresolved-placeholder error. If the placeholder doesn't match the scheme of `returnUrl`, the SDK throws `PLATFORM_ERROR` before it opens checkout.
    </Note>
  </Step>
</Steps>

## Usage

The SDK has two ways to start checkout: an activity result launcher and a suspend function. Both return the same `CheckoutResult`.

<Tabs>
  <Tab title="Launcher (Recommended)">
    Register the contract with `registerForActivityResult`, then launch it:

    ```kotlin theme={null}
    import com.dodopayments.checkout.CheckoutParams
    import com.dodopayments.checkout.CheckoutStatus
    import com.dodopayments.checkout.DodoCheckout

    // showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
    // are your own functions. See Abandoned Sessions for what to reconcile.
    private val checkoutLauncher =
        registerForActivityResult(DodoCheckout.contract()) { result ->
            when (result.status) {
                CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId)
                CheckoutStatus.FAILED -> showFailure()
                CheckoutStatus.CANCELLED -> reconcileAbandonedSession() // outcome unknown, not a failure
                CheckoutStatus.PENDING -> reconcileAbandonedSession()
                CheckoutStatus.EXPIRED -> showExpired()
            }
        }

    checkoutLauncher.launch(
        CheckoutParams(
            checkoutUrl = checkoutUrl, // from your backend's checkout session
            returnUrl = "myapp://checkout/return"
        )
    )
    ```

    <Tip>
      Use this style when you can. Android delivers the result through its `ActivityResultRegistry`, so the result survives process death.
    </Tip>
  </Tab>

  <Tab title="Suspend Function">
    Call `DodoCheckout.start` from a coroutine scope:

    ```kotlin theme={null}
    import androidx.lifecycle.lifecycleScope
    import com.dodopayments.checkout.CheckoutParams
    import com.dodopayments.checkout.CheckoutStatus
    import com.dodopayments.checkout.DodoCheckout
    import kotlinx.coroutines.launch

    // showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
    // are your own functions. See Abandoned Sessions for what to reconcile.
    lifecycleScope.launch {
        val result = DodoCheckout.start(
            activity = this@MyActivity,
            params = CheckoutParams(
                checkoutUrl = checkoutUrl, // from your backend's checkout session
                returnUrl = "myapp://checkout/return"
            ),
            onEvent = { event -> println(event.name) } // logging only
        )

        when (result.status) {
            CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId)
            CheckoutStatus.FAILED -> showFailure()
            CheckoutStatus.CANCELLED -> reconcileAbandonedSession() // outcome unknown, not a failure
            CheckoutStatus.PENDING -> reconcileAbandonedSession()
            CheckoutStatus.EXPIRED -> showExpired()
        }
    }
    ```

    `onEvent` receives `checkout.opened`, `checkout.return_received`, and `checkout.closed` events. Use them for logging only, never to decide the outcome.

    <Warning>
      This style waits on an in-memory `CompletableDeferred`, so the result does **not** survive process death. `onEvent` is available only in this style, not with the launcher.
    </Warning>
  </Tab>
</Tabs>

## What the Result Means

The SDK builds `CheckoutResult` from the query parameters on the return URL.

<Warning>
  The `status` field is a UI hint, not proof of payment. Before you grant access, confirm the payment on your backend with a webhook or the Get Payment Detail endpoint.
</Warning>

<ParamField body="status" type="CheckoutStatus" required>
  One of five values:

  * `SUCCEEDED`: the return URL has `status=succeeded` (one-time payment) or `status=active` (subscription).
  * `FAILED`: the payment was declined (`status=failed`).
  * `CANCELLED`: the customer closed the Custom Tab before the return URL arrived. The SDK doesn't know the outcome, and the payment may have succeeded, so don't show a failure screen. Reconcile the [abandoned session](#abandoned-sessions) instead.
  * `PENDING`: the payment settles later (`status=processing` or any `requires_*` value), or the `status` parameter was missing or unrecognized. Reconcile it like `CANCELLED`.
  * `EXPIRED`: the checkout session expired (`status=expired`).
</ParamField>

<ParamField body="paymentId" type="String?">
  The `payment_id` query parameter, when the return URL includes one. Show it in your UI, but don't use it to grant access. See [Verify the Payment](#verify-the-payment).
</ParamField>

<ParamField body="subscriptionId" type="String?">
  The `subscription_id` query parameter. Set for subscription checkouts.
</ParamField>

<ParamField body="licenseKeys" type="List<String>?">
  The `license_key` query parameter. Set when the checkout includes license key products.
</ParamField>

<ParamField body="customerEmail" type="String?">
  The `email` query parameter. Set when checkout captures an email address.
</ParamField>

<ParamField body="raw" type="Map<String, String>">
  Every query parameter from the return URL, verbatim.
</ParamField>

## Verify the Payment

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/developer-resources/webhooks">
    Listen for payment events in real time.
  </Card>

  <Card title="Get Payment Detail" icon="magnifying-glass" href="/api-reference/payments/get-payments-1">
    Query the payment status on demand.
  </Card>
</CardGroup>

Grant access only after one of these confirms the payment, for example with the `payment.succeeded` or `subscription.active` webhook. Don't rely on `CheckoutResult.status` alone.

## Appearance Customization

To change the Custom Tab's toolbar, buttons, and color scheme, pass a `BrowserCustomization` as `customization` on `CheckoutParams`. Every field is optional and defaults to `null`. For a `null` field, the SDK doesn't set that option, so the browser that hosts the Custom Tab applies its own default.

<ParamField body="toolbarColor" type="Int?">
  Toolbar background color, as an ARGB `Color` int.
</ParamField>

<ParamField body="navigationBarColor" type="Int?">
  Navigation bar color, as an ARGB `Color` int.
</ParamField>

<ParamField body="navigationBarDividerColor" type="Int?">
  Color of the divider above the navigation bar, as an ARGB `Color` int.
</ParamField>

<ParamField body="closeButtonStyle" type="CloseButtonStyle?">
  `DEFAULT` shows the system "X" icon. `BACK` shows a back arrow that the SDK draws.
</ParamField>

<ParamField body="closeButtonPosition" type="CloseButtonPosition?">
  The side of the toolbar where the close button appears: `START` or `END`.
</ParamField>

<ParamField body="shareButtonEnabled" type="Boolean?">
  Shows the toolbar's share icon. `false` hides it.
</ParamField>

<ParamField body="showTitleEnabled" type="Boolean?">
  Shows the page title under the URL in the toolbar.
</ParamField>

<ParamField body="urlBarHidingEnabled" type="Boolean?">
  Hides the toolbar automatically as the page scrolls.
</ParamField>

<ParamField body="bookmarksButtonEnabled" type="Boolean?">
  Shows "Bookmark this page" in the overflow menu.
</ParamField>

<ParamField body="downloadsButtonEnabled" type="Boolean?">
  Shows "Download page" in the overflow menu.
</ParamField>

<ParamField body="colorScheme" type="ColorScheme?">
  `LIGHT` or `DARK` forces that appearance regardless of the device's system setting. `SYSTEM` follows the system setting.
</ParamField>

This example reuses `checkoutLauncher` from [Usage](#usage):

```kotlin theme={null}
import android.graphics.Color
import com.dodopayments.checkout.BrowserCustomization
import com.dodopayments.checkout.CheckoutParams

checkoutLauncher.launch(
    CheckoutParams(
        checkoutUrl = checkoutUrl,
        returnUrl = "myapp://checkout/return",
        customization = BrowserCustomization(
            toolbarColor = Color.parseColor("#6366F1"),
            closeButtonStyle = BrowserCustomization.CloseButtonStyle.BACK,
            colorScheme = BrowserCustomization.ColorScheme.DARK,
        )
    )
)
```

## Errors

`DodoCheckout.start` throws `CheckoutError` only for misuse or a platform failure. Read the reason from `CheckoutError.code`:

* `INVALID_CHECKOUT_URL`: `checkoutUrl` isn't an `https` checkout session URL (path starting with `/session/`) on `checkout.dodopayments.com` or `test.checkout.dodopayments.com`.
* `INVALID_RETURN_URL`: `returnUrl` isn't an absolute URL with a scheme and a host.
* `ALREADY_IN_PROGRESS`: another checkout is running. Only one checkout can run at a time.
* `PLATFORM_ERROR`: an unexpected platform failure, including a `returnUrl` scheme that doesn't match your `dodoCallbackScheme` placeholder.

A customer who cancels, or a declined payment, is always a result (`CANCELLED` or `FAILED`), never a thrown error. With the launcher, validation errors throw from `launcher.launch(...)`. A platform failure after the launch can't be thrown through the activity result callback, so the launcher returns `CANCELLED` with the error code in `raw["error"]`.

## Abandoned Sessions

The SDK records the checkout session when checkout starts, and clears the record only when checkout ends with `SUCCEEDED`, `FAILED`, or `EXPIRED`. The record stays when the app is killed during checkout, and after a `CANCELLED` or `PENDING` result, because in those cases the SDK doesn't know the outcome. Check for it on the next app launch and after every `CANCELLED` or `PENDING` result:

```kotlin theme={null}
// context is any Context, such as your Activity.
DodoCheckout.getAbandonedSession(context)?.let { abandoned ->
    // Ask your backend for the outcome of abandoned.sessionId, and show it.
    // Clear the record only after the outcome is final:
    DodoCheckout.clearAbandonedSession(context)
}
```

`abandoned.sessionId` is the checkout session ID, which starts with `cks_`. `abandoned.createdAt` is the time checkout started, as an epoch timestamp in milliseconds. Your backend can look up the session with [Get Checkout Session](/api-reference/checkout-sessions/get-checkouts), which returns its `payment_id` and `payment_status`. Until the payment reaches a final status, treat it as pending, not failed.

## Related

<CardGroup cols={2}>
  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    Best practices for mobile checkout flows.
  </Card>

  <Card title="Kotlin SDK" icon="code" href="/developer-resources/sdks/kotlin">
    Backend SDK for server-side operations.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.