Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 4: Subscriptions with RevenueCat

Working with subscriptions directly means navigating the three-tier Google Play hierarchy: Subscription → Base Plan → Offer. You call queryProductDetailsAsync() and get back nested SubscriptionOfferDetails objects you must parse and select from manually.

With RevenueCat, the three-tier hierarchy is still there on the Google Play side, but the SDK presents it through a simpler abstraction: Offerings → Packages → SubscriptionOptions.

How RevenueCat Maps the Hierarchy

In the RevenueCat dashboard, you map Google Play products and base plans to Packages inside Offerings. The mapping looks like this:

Google Play

RevenueCat

Subscription (product ID)

Product

Base Plan

SubscriptionOption (base plan)

Offer

SubscriptionOption (offer)

Group of base plans in an Offering

Package

A Package has a product property of type StoreProduct. A StoreProduct for a subscription has a subscriptionOptions list, where each SubscriptionOption represents either a base plan or an offer.

Fetching Subscription Products

kotlin
val offerings = Purchases.sharedInstance.awaitOfferings()
val offering = offerings.current ?: return

// Access common package types directly
val monthly = offering.monthly
val annual = offering.annual
val weekly = offering.weekly

// Or iterate all packages
for (pkg in offering.availablePackages) {
    val product = pkg.product
    val price = product.price.formatted
    val period = product.period?.iso8601     // "P1M", "P1Y", etc. (null for INAPP)
    val title = product.title
}

The PackageType enum gives you named access to standard durations: MONTHLY, ANNUAL, WEEKLY, TWO_MONTH, THREE_MONTH, SIX_MONTH, LIFETIME. Custom package types have PackageType.CUSTOM.

Selecting the Right SubscriptionOption

When you pass a Package to PurchaseParams, the SDK selects the defaultOption automatically. The default option selection logic:

  1. Filters out options tagged "rc-ignore-offer" or "rc-customer-center"
  2. Chooses the option with the longest free trial or cheapest first phase
  3. Falls back to the base plan if no offers qualify

This means users automatically get the best available offer without you writing selection logic.

If you want to present a specific offer to the user, for example, a win-back offer only for returned subscribers, access the SubscriptionOption directly:

kotlin
val product = offering.monthly?.product ?: return
val subscriptionOptions = product.subscriptionOptions

// Find a specific offer by its Google Play offer ID or tag
val winBackOption = subscriptionOptions?.firstOrNull { option ->
    option.tags.contains("win-back")
}

val optionToPurchase = winBackOption ?: product.defaultOption ?: return
val params = PurchaseParams.Builder(activity, optionToPurchase).build()

Displaying Trial and Introductory Pricing

Each SubscriptionOption has a pricingPhases list. The first phase may be a free trial or discounted intro price:

kotlin
val option = pkg.product.defaultOption ?: return
val firstPhase = option.pricingPhases.first()

// Use offerPaymentMode for the clearest free trial detection
val isTrial = firstPhase.offerPaymentMode == OfferPaymentMode.FREE_TRIAL

if (isTrial) {
    // billingPeriod.value is the count of the period's unit (e.g., 1 for P1W, not 7)
    // Use billingPeriod.unit to build a correct label
    val period = firstPhase.billingPeriod
    val trialLabel = when (period.unit) {
        Period.Unit.DAY -> "${period.value}-day"
        Period.Unit.WEEK -> "${period.value}-week"
        Period.Unit.MONTH -> "${period.value}-month"
        Period.Unit.YEAR -> "${period.value}-year"
        else -> period.iso8601
    }
    showLabel("Start your $trialLabel free trial")
} else {
    showLabel(firstPhase.price.formatted)
}

Free Trial Eligibility

Google Play returns only the offers a user is eligible for in queryProductDetailsAsync(). RevenueCat passes through whatever Google returns in subscriptionOptions without additional eligibility filtering. The SDK's defaultOption selection logic does filter out options tagged "rc-ignore-offer" or "rc-customer-center", but it does not perform trial eligibility checking, that is handled at the Google Play layer before data reaches the SDK.

If a user has already used their free trial for a product, Google simply will not include that trial offer in the returned options. The base plan option will be selected as defaultOption in that case.

Prepaid Plans

Prepaid base plans work the same way as standard base plans from the RevenueCat API perspective. The SubscriptionOption for a prepaid plan will have pricingPhases with RecurrenceMode.NON_RECURRING.

To support pending purchases for prepaid plans, enable the option during SDK configuration:

kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .pendingTransactionsForPrepaidPlansEnabled(true)
    .build()

Checking Active Subscriptions

After a successful purchase:

kotlin
val customerInfo = result.customerInfo

// Via entitlements (recommended)
val isSubscribed = customerInfo.entitlements["pro"]?.isActive == true

// Via active subscriptions (if you need the product ID)
val activeSubs = customerInfo.activeSubscriptions
// Returns Set<String> of "subscriptionId:basePlanId" strings

RevenueCat computes isActive server-side, accounting for grace period, account hold, cancellation with remaining time, and expiry. You do not replicate that logic.

Prefer building from scratch?

The Google Play Billing Handbook covers the same topics with raw BillingClient, Developer API, and RTDNs.

Related chapters

  • Chapter 6: The Purchase Flow

    awaitPurchase() handles the complete billing flow, verification, and acknowledgement internally.

    Learn more
  • Chapter 7: Subscription Upgrades and Downgrades

    Same replacement modes, one PurchaseParams builder, and no manual token chain code to maintain.

    Learn more
  • Chapter 11: Subscription States

    Seven complex subscription states are resolved to just one simple boolean check: isActive.

    Learn more
Subscriptions with RevenueCat | RevenueCat