Rovenue SDK
Platforms

Android (Kotlin)

Install and wire the Rovenue SDK in an Android/Kotlin app.

Add the dependency

Add the SDK to your module-level build.gradle.kts:

dependencies {
  implementation("io.rovenue:sdk:0.16.0")
}

Or in Groovy DSL (build.gradle):

dependencies {
  implementation 'io.rovenue:sdk:0.16.0'
}

The SDK drives Play Billing natively. Receipt processing and server-side validation happen automatically via the Rovenue API — you do not need to call Play Billing directly.

Configure the SDK

Call Rovenue.configure once in your Application.onCreate(), before any component starts:

import android.app.Application
import dev.rovenue.sdk.Rovenue
import dev.rovenue.sdk.generated.LogLevel

class App : Application() {

  override fun onCreate() {
    super.onCreate()

    val versionName = packageManager
      .getPackageInfo(packageName, 0)
      .versionName

    Rovenue.configure(
      apiKey = "rov_pub_YOUR_API_KEY",
      logLevel = LogLevel.INFO,
      appVersion = versionName,
      context = this,
    )
  }
}

Pass context (the Application itself works, or any Context — the SDK keeps context.applicationContext). Without it, getOfferings() still works, but purchase() and restorePurchases() throw RovenueException(kind = ErrorKind.STORE_PROBLEM) — Play Billing needs a Context to bind to, and there is no fallback.

Self-hosting? baseUrl is optional and defaults to the hosted endpoint https://api.rovenue.io. Override it to point at your own deployment, e.g. Rovenue.configure(apiKey = "rov_pub_YOUR_API_KEY", baseUrl = "https://api.acme.com", appVersion = versionName). The host must use https:// — plain http:// is accepted only for localhost during local development.

Register App in your AndroidManifest.xml:

<application android:name=".App" ...>

Types on this page

// Kotlin mirror of Rovenue.configure's parameters — there is no separate
// RovenueConfig type on this platform, only named parameters.
fun configure(
  apiKey: String,
  baseUrl: String? = null,           // optional — defaults to https://api.rovenue.io
  logLevel: LogLevel = LogLevel.WARN, // OFF | ERROR | WARN | INFO | DEBUG | TRACE
  appVersion: String? = null,
  context: Context? = null,          // required for purchase()/restorePurchases()
  environment: String? = null,
)
FieldTypeDescription
apiKeyStringPublic SDK key from your Rovenue dashboard. Prefix rov_pub_.
baseUrlString?Optional. Defaults to https://api.rovenue.io. Override only when self-hosting (https://; http:// allowed for localhost only).
logLevelLogLevelVerbosity of SDK logs. Defaults to WARN — turn up to INFO/DEBUG locally, never ship DEBUG/TRACE in production.
appVersionString?Overrides the version read from the package manager.
contextContext?An Android Context (the SDK stores context.applicationContext). Pass it — purchase() and restorePurchases() throw STORE_PROBLEM without it.
environmentString?Optional environment label forwarded with requests.

Call configure in Application.onCreate — not inside an Activity. Calling any other SDK method before configure has run throws a plain IllegalStateException — not a RovenueException — so catch (e: RovenueException) will not observe it. This is a programming error to fix by ordering (always configure first), not a condition to catch. See Calling the SDK before configure().

Wire foreground signals

Use ProcessLifecycleOwner from androidx.lifecycle to observe the process-level foreground state. Add the observer in Application.onCreate, after the configure call:

import android.app.Application
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import dev.rovenue.sdk.Rovenue

class App : Application() {

  override fun onCreate() {
    super.onCreate()

    val versionName = packageManager
      .getPackageInfo(packageName, 0)
      .versionName

    Rovenue.configure(
      apiKey = "rov_pub_YOUR_API_KEY",
      appVersion = versionName,
      context = this,
    )

    ProcessLifecycleOwner.get().lifecycle.addObserver(
      object : DefaultLifecycleObserver {
        override fun onStart(owner: LifecycleOwner) {
          Rovenue.shared.setForeground(true)
        }
        override fun onStop(owner: LifecycleOwner) {
          Rovenue.shared.setForeground(false)
        }
      }
    )
  }
}

ProcessLifecycleOwner fires onStart/onStop at the process level — not per-Activity. This is the correct granularity for the SDK's polling cadence.


Purchases

Use getOfferings() to fetch products configured in your Rovenue dashboard, then purchase(pkg) to drive the full Play Billing flow. The SDK validates the receipt server-side and resolves with updated entitlements.

import dev.rovenue.sdk.Rovenue
import dev.rovenue.sdk.RovenueException
import dev.rovenue.sdk.generated.ErrorKind

val offerings = Rovenue.shared.getOfferings()
val pkg = offerings.current?.packages?.firstOrNull()

if (pkg != null) {
  try {
    // `activity` is your current Activity — Play Billing requires it.
    val result = Rovenue.shared.purchase(activity, pkg)
    if (result.isDeferred) {
      // Ask-to-Buy / parental-control hold — no entitlements yet. The SDK
      // reconciles it in the background; collect Rovenue.shared.changes for
      // ChangeEvent.ENTITLEMENTS_CHANGED to know when it resolves.
    } else {
      // result.entitlements and result.virtualCurrencies are already up to date
    }
  } catch (e: RovenueException) {
    when (e.kind) {
      ErrorKind.PURCHASE_CANCELED -> Unit // User dismissed the store sheet — no action needed
      else -> throw e
    }
  }
}

// Re-validate all prior transactions (e.g. after reinstall):
Rovenue.shared.restorePurchases(activity)
// Kotlin mirror of PurchaseResult
data class PurchaseResult(
  val entitlements: List<Entitlement>,
  val virtualCurrencies: Map<String, Long>,
  val productId: String,
  val storeTransactionId: String,
  val isDeferred: Boolean, // true for an Ask-to-Buy / parental-control hold
)

data class Entitlement(
  val id: String,
  val isActive: Boolean,
  val productIdentifier: String,
  val store: String,
  val expiresIso: String?, // ISO 8601 — present for time-limited entitlements
)
FieldTypeDescription
entitlementsList<Entitlement>Updated entitlement list after the purchase.
virtualCurrenciesMap<String, Long>Updated virtual-currency balances after the purchase (currency code → amount).
productIdStringThe product identifier that was purchased.
storeTransactionIdStringThe Play Store transaction ID.
isDeferredBooleantrue when the purchase is pending external approval — no entitlements are granted yet.

Who acknowledges a Play purchase

The SDK is the only party that acknowledges (subscriptions, non-consumables) or consumes (consumables) a Google Play purchase; the Rovenue server never does. It does so on the device, only after the server has verified the receipt, and it re-checks the token's state with Play first, so a purchase that is already acknowledged is left alone. Once the server has verified the purchase, purchase() returns its PurchaseResult and never throws: if Play cannot take the acknowledgement right then (the device went offline, Play Billing is unavailable), the SDK retries a few times and then leaves it to the next reconcile, which runs on every app launch and foreground — well inside Play's three-day window before it auto-refunds an unacknowledged purchase. A verification failure is never acknowledged, so Play refunds that purchase as intended.

There is no PurchaseCancelledException/PurchasePendingException — every SDK method throws the single RovenueException, discriminated by .kind. And there is no PURCHASE_PENDING kind at all: as of 0.16.0, a deferred purchase does not throw; it returns normally with isDeferred = true. See Reference: Errors for the full 24-value ErrorKind taxonomy and Processing Purchases for the complete purchase API and the StoreProduct shape.


Funnels

getFunnel(placementId, locale) resolves a placement into the funnel the current subscriber should see (see Placements & Funnels). The session is already open when it returns — hand it to a FunnelDriver and host RovenueFunnelView (see Show a Funnel):

val funnel = Rovenue.shared.getFunnel(placementId = "onboarding", locale = "en")
if (funnel != null) {
  // null when the placement has no assignment — render nothing.
  // Hand `funnel` to a FunnelDriver and host RovenueFunnelView (Show a Funnel).
  println(funnel)
}

Purchase attribution is automatic: the funnel's presentedContext is attached to the next purchase for you.

See Placements for how placements resolve to a funnel or an A/B experiment variant.


Observing changes

Rovenue.shared.changes is a SharedFlow of change events. Collect it from a coroutine scope tied to your component's lifecycle:

import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.Lifecycle
import dev.rovenue.sdk.Rovenue

// Inside an Activity or Fragment:
lifecycleScope.launch {
  repeatOnLifecycle(Lifecycle.State.STARTED) {
    Rovenue.shared.changes.collect { event ->
      // event: ChangeEvent (ENTITLEMENTS_CHANGED | IDENTITY_CHANGED | VIRTUAL_CURRENCIES_CHANGED)
      println("Rovenue change: $event")
    }
  }
}

Collect Rovenue.shared.changes from an Activity or ViewModel scope — not from inside the SDK's own internal scope. Calling Rovenue.shared.shutdown() from inside an active collector's coroutine may interrupt the collector mid-flight.


Suspend functions

SDK methods such as identify, logOut, refreshEntitlements, and refreshVirtualCurrencies are suspend functions. Call them from a coroutine:

lifecycleScope.launch {
  // After your auth flow completes:
  Rovenue.shared.identify("user_123")

  // When your user signs out:
  Rovenue.shared.logOut()

  val entitlements = Rovenue.shared.refreshEntitlements()
}

See Identifying Users for the full identity model, including what happens when identify and logOut are called.


Funnels

A placement can serve a native funnel: getFunnel, then a FunnelDriver bound to RovenueFunnelView (packages/sdk-kotlin/src/main/kotlin/dev/rovenue/sdk/Rovenue.kt:631) — full path: Show a funnel.

import dev.rovenue.sdk.funnelui.FunnelDriver
import dev.rovenue.sdk.funnelui.UniffiFunnelSession

val funnel = Rovenue.shared.getFunnel(placementId = "onboarding")
if (funnel != null) {
    val driver = FunnelDriver(UniffiFunnelSession(funnel.session))
    println(driver.stateJson())
}

What's next

On this page