> This page is for Afterpay.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.cash.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.cash.app/_mcp/server.

# Built-in UI on Android

With Built-in UI, the SDK opens Afterpay's checkout right away and shows a loading state while your app gets a checkout token from your server. Because the checkout is open before the token exists, the customer sees a response the moment they tap your button. Built-in UI also supports [express checkout](/cash-app-afterpay/guides/api-development/additional-features/express-checkout), where the customer picks a shipping address and shipping option inside checkout.

The SDK calls Built-in UI Checkout V2.

## Step 1: Create a checkout on your server

When the SDK asks for a token, your app calls your server, and your server calls [Create Checkout](/cash-app-afterpay/api-reference/reference/checkouts/create-checkout-1) and returns the `token` from the response.

```json
{
  "amount": { "amount": "100.00", "currency": "USD" },
  "consumer": { "email": "test@example.com" },
  "merchant": {
    "redirectConfirmUrl": "https://example.com/checkout/confirm",
    "redirectCancelUrl": "https://example.com/checkout/cancel",
    "popupOriginUrl": "https://static.afterpay.com"
  },
  "merchantReference": "merchant-order-123"
}
```

> **Warning**
>
> Set `merchant.popupOriginUrl` to `https://static.afterpay.com`. Otherwise, the checkout doesn't behave as expected.

For express checkout, also set `mode` to `express`. For the other express fields, see [Express checkout](/cash-app-afterpay/guides/api-development/additional-features/express-checkout).

## Step 2: Set a checkout handler

The SDK calls your `AfterpayCheckoutV2Handler` to get the checkout token and, for express checkout, the shipping options. Register it with `Afterpay.setCheckoutV2Handler` before you start checkout. The SDK holds the handler in a global reference until you replace it or pass `null`.

```kotlin
private val checkoutHandler = object : AfterpayCheckoutV2Handler {
  override fun didCommenceCheckout(
    onTokenLoaded: (Result<String>) -> Unit,
  ) {
    lifecycleScope.launch {
      // Call your server's checkout endpoint.
      val token = runCatching { checkoutService.createCheckout() }
      onTokenLoaded(token)
    }
  }

  override fun shippingAddressDidChange(
    address: ShippingAddress,
    onProvideShippingOptions: (ShippingOptionsResult) -> Unit,
  ) {
    // Express checkout only. See Step 4.
  }

  override fun shippingOptionDidChange(
    shippingOption: ShippingOption,
    onProvideShippingOption: (ShippingOptionUpdateResult?) -> Unit,
  ) {
    // Express checkout only. See Step 4.
  }
}

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  Afterpay.setCheckoutV2Handler(checkoutHandler)
}

override fun onDestroy() {
  // The SDK holds the handler globally. Clear it to avoid leaking the activity.
  Afterpay.setCheckoutV2Handler(null)
  super.onDestroy()
}
```

If you pass a failure to `onTokenLoaded`, the SDK shows a dialog that lets the customer retry or close checkout. Call `onTokenLoaded` on the main thread. The SDK shows the error dialog on the calling thread.

## Step 3: Start the checkout

Launch the intent from `createCheckoutV2Intent` with an activity result launcher:

```kotlin
private val checkoutLauncher =
  registerForActivityResult(StartActivityForResult()) { result ->
    val data = result.data ?: return@registerForActivityResult
    when (result.resultCode) {
      RESULT_OK -> {
        val token = Afterpay.parseCheckoutSuccessResponse(data)
        // Send the token to your server to complete the order.
      }
      RESULT_CANCELED -> {
        val status = Afterpay.parseCheckoutCancellationResponse(data)
        // Handle the cancellation.
      }
    }
  }

private fun onAfterpayButtonClicked() {
  val options = AfterpayCheckoutV2Options(buyNow = true)
  checkoutLauncher.launch(Afterpay.createCheckoutV2Intent(this, options))
}
```

If you haven't set the SDK configuration, or the customer's language isn't supported in your region, checkout closes with the cancellation status `LANGUAGE_NOT_SUPPORTED`. If you haven't set the handler, it closes with `NO_CHECKOUT_HANDLER`. If the customer closes checkout, the status is `USER_INITIATED`.

### Checkout options

Pass `AfterpayCheckoutV2Options` to change the customer's experience in checkout:

| Option                   | Effect                                                                                                                                                                                                     |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `buyNow`                 | Shows a **Buy Now** button at the end of checkout, which signals that you capture the payment right away. Set it to `true` only if you capture the payment immediately, without a review step in your app. |
| `pickup`                 | For express checkout. Lets you set a pickup address in the `shipping` object when you create the checkout, and stops the customer from choosing a delivery address.                                        |
| `shippingOptionRequired` | For express checkout. Set it to `false` to stop the customer from choosing a shipping option in checkout.                                                                                                  |

## Step 4: Provide shipping options (express checkout)

For express checkout, the SDK calls your handler when the customer chooses a shipping address and when they choose a shipping option. Return the shipping options for the address, or an error:

```kotlin
override fun shippingAddressDidChange(
  address: ShippingAddress,
  onProvideShippingOptions: (ShippingOptionsResult) -> Unit,
) {
  val usd = Currency.getInstance("USD")
  val standard = ShippingOption(
    id = "standard",
    name = "Standard",
    description = "3-5 days",
    shippingAmount = Money(BigDecimal("5.00"), usd),
    orderAmount = Money(BigDecimal("105.00"), usd),
    taxAmount = null,
  )
  onProvideShippingOptions(ShippingOptionsSuccessResult(listOf(standard)))
}

override fun shippingOptionDidChange(
  shippingOption: ShippingOption,
  onProvideShippingOption: (ShippingOptionUpdateResult?) -> Unit,
) {
  // Return an updated amount for the option, or null to keep it.
  onProvideShippingOption(null)
}
```

* The SDK calls both methods on a background thread. Call your server from them, and pass the result to the callback from any thread.
* `orderAmount` is the order total including shipping and tax.
* To reject an address, pass `ShippingOptionsErrorResult` with `SHIPPING_ADDRESS_UNRECOGNIZED`, `SHIPPING_ADDRESS_UNSUPPORTED`, `SERVICE_UNAVAILABLE`, or `BAD_RESPONSE`.
* To update the selected option, pass `ShippingOptionUpdateSuccessResult` with a `ShippingOptionUpdate`. To report an error, pass `ShippingOptionUpdateErrorResult` with `SERVICE_UNAVAILABLE` or `BAD_RESPONSE`.

## Step 5: Complete the order

Send the order token to your server. To capture the full amount immediately, your server calls [Capture Full Payment](/cash-app-afterpay/api-reference/reference/payments/capture-full-payment) with the token. To capture later, call [Auth](/cash-app-afterpay/api-reference/reference/payments/auth) and then [Capture Payment](/cash-app-afterpay/api-reference/reference/payments/capture-payment).