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

# EncorePurchaseController

> The single purchase path: your billing code, invoked by the SDK

The single purchase path: the SDK never runs purchase code you didn't write. You register a controller at [`configure()`](./configure); when a user accepts an offer that leads to a purchase, Encore invokes it with the product to buy and awaits your verdict.

## Definition

```kotlin theme={null}
interface EncorePurchaseController {
    suspend fun purchase(activity: Activity, request: PurchaseRequest): EncorePurchaseResult
}

enum class EncorePurchaseResult {
    Purchased,
    Cancelled,
    Pending,    // Deferred (parental approval / SCA); may complete later via Google Play
}
```

## PurchaseRequest

What your controller receives when Encore needs a purchase.

```kotlin theme={null}
data class PurchaseRequest(
    val productId: String,            // Google Play product to purchase
    val placementId: String?,         // the placement that triggered it
    val promoOfferId: String? = null, // promotional offer, when applicable
    val basePlanId: String? = null,   // base plan to select, for multi-base-plan products
)
```

| Property       | Type      | Description                                                                                                                               |
| -------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `productId`    | `String`  | The Google Play product identifier to purchase                                                                                            |
| `placementId`  | `String?` | The placement that triggered this purchase                                                                                                |
| `promoOfferId` | `String?` | Promotional offer identifier, when applicable                                                                                             |
| `basePlanId`   | `String?` | Google Play base plan ID, when the product exposes multiple base plans. Select this base plan when routing through your own billing layer |

## Implementations

<CodeGroup>
  ```kotlin RevenueCat theme={null}
  class RevenueCatController : EncorePurchaseController {
      override suspend fun purchase(
          activity: Activity,
          request: PurchaseRequest,
      ): EncorePurchaseResult {
          return try {
              val product = Purchases.sharedInstance
                  .awaitGetProducts(listOf(request.productId))
                  .firstOrNull() ?: error("Product not in RC catalog: ${request.productId}")
              Purchases.sharedInstance.awaitPurchase(
                  PurchaseParams.Builder(activity, product).build()
              )
              EncorePurchaseResult.Purchased
          } catch (e: PurchasesTransactionException) {
              if (e.userCancelled) EncorePurchaseResult.Cancelled else throw e
          }
      }
  }
  ```

  ```kotlin Your own billing stack theme={null}
  class AppPurchases(private val billing: MyBillingStack) : EncorePurchaseController {
      // Runs on Dispatchers.Main.immediate. Throw for real failures; map
      // "user cancelled" to Cancelled. Do not retain or finish the activity.
      override suspend fun purchase(
          activity: Activity,
          request: PurchaseRequest,
      ): EncorePurchaseResult {
          return when (billing.purchase(activity, request.productId, request.promoOfferId)) {
              MyOutcome.Success -> EncorePurchaseResult.Purchased
              MyOutcome.Cancelled -> EncorePurchaseResult.Cancelled
              MyOutcome.Deferred -> EncorePurchaseResult.Pending // parental approval / SCA
          }
      }
  }
  ```
</CodeGroup>

A production Play Billing controller also connects the billing client, launches the flow on the `activity` you receive, and acknowledges the purchase before returning `Purchased`. Follow [Google's Play Billing integration guide](https://developer.android.com/google/play/billing/integrate) for those steps; the contract below is everything Encore requires of you.

## The contract

* One invocation returns one verdict, or throws. A returned value lands on the result record as the `publisher` axis (`PublisherOutcome.Purchased` / `Cancelled` / `Pending`) and on [`Encore.outcomes`](./outcomes); a throw lands as `PublisherOutcome.Failed`.
* The controller is invoked on `Dispatchers.Main.immediate`. Usually this happens inside the sheet's lifetime, so your billing UI overlays the sheet and the sheet resolves after you return. One exception: a purchase-first variant invokes your controller over your own Activity before any Encore UI exists, and returning `Cancelled` there means no sheet is shown and the result is `NotPresented(IapFirstDeclined)`.
* The `activity` parameter is valid only for the duration of the call. Do not retain it; do not `finish()` it.
* A thrown exception carries your error type into analytics (`controller_threw: <Type>`); cancelling your own coroutine carries only the generic `controller_threw: CancellationException`. Bound your own pre-flight steps (connecting a billing client, catalog lookups) with short timeouts; only the user-facing purchase dialog deserves long waits.
* A controller that never resolves is abandoned as failed after 5 minutes. A purchase that completes late within a bounded window is still recorded and linked; beyond that, the Play server notification backstops the money facts.

## Related

* [configure()](./configure), which accepts the controller and defines what happens when none is registered
* [PresentationResult](./presentation-result), where the verdict lands as the `publisher` axis
* [Configure the SDK](../quickstart/configure#register-a-purchase-controller) for the registration walkthrough
* [Updating to 2.x](../guides/updating-to-2-x) if you are migrating an existing integration
