Subscription monetization on Android has evolved significantly over the years. With the introduction of base plans and offers in 2022, Google gave developers more flexibility in structuring their subscription products. Now, Google has taken another major step forward with Subscription with Add-ons, also known as multi-line subscriptions or multi-product checkout for subscriptions. This feature allows developers to bundle multiple subscription products together into a single purchase, creating a more streamlined experience for both users and developers.
Read on to explore what multi-line subscriptions are, discuss practical strategies for using them effectively, walk through the implementation details using the Play Billing Library directly, and finally examine how RevenueCat can simplify the entire process.
What are multi-line subscriptions?
At its core, Subscription with Add-ons is a feature that lets you bundle multiple subscription products together so they can be purchased, billed, and managed as a unified subscription. Rather than requiring users to make separate purchases for a base subscription and additional premium features, you can now offer everything in a single checkout flow.
Consider a music streaming application that offers a base ‘Premium’ subscription. Previously, if you wanted to offer additional features like ‘HiFi Audio’ or ‘Offline Downloads’ as separate paid add-ons, users would need to purchase and manage each subscription independently. This meant multiple transactions, multiple renewal dates, and multiple entries in their subscription management screen. With Subscription with Add-ons, users can select the base Premium plan along with any combination of add-ons and complete a single purchase. They see one combined price, go through one checkout flow, and have everything synchronized to a single renewal date.
The terminology can be a bit confusing at first. Google uses ‘Subscription with Add-ons’ as the official feature name, but you might also see it referred to as ‘multi-line subscriptions’ (referring to the multiple line items in a single purchase) or ‘multi-product checkout’ (emphasizing the checkout experience). These all refer to the same capability.
How the bundle works
When a user purchases a Subscription with Add-ons, the first item in the product list becomes the base item, and all subsequent items are treated as add-ons. This distinction is important because the base item determines certain behaviors for the entire bundle. For example, when the base subscription is canceled, all associated add-ons are automatically canceled as well. The add-ons cannot exist independently of the base subscription.
Google Play handles the complexity of aligning billing cycles automatically. When a user adds a new add-on to an existing subscription, Google Play calculates a prorated charge to align the add-on’s renewal date with the base item. This means that after the initial prorated period, all items in the bundle renew together on the same date. Similarly, if a user removes an add-on, it continues to provide access until the end of its current billing period but will not renew.
Key constraints to understand
Before diving into implementation, there are several important constraints that shape how you can use this feature.
- All items in a Subscription with Add-ons must have the same billing period. You cannot combine an annual base subscription with monthly add-ons, or vice versa. If your base plan bills monthly, all add-ons must also bill monthly. This constraint exists because Google Play needs to synchronize renewal dates across all items.
- This feature is only available for auto-renewing subscriptions. Prepaid subscriptions, which have a fixed duration and do not automatically renew, cannot be used as the base item or as add-ons.
- There’s a maximum limit of 50 items in a single Subscription with Add-ons purchase. While most applications will never approach this limit, it is worth knowing if you are building a highly modular subscription system.
- This feature isn’t available in all regions. As of the current documentation, India and South Korea don’t support Subscription with Add-ons. You will need to provide alternative purchase flows for users in these regions.
- Finally, pausing and resuming subscriptions is not supported for subscriptions that have add-ons. If your application relies heavily on the pause feature, you will need to consider whether the benefits of add-ons outweigh this limitation.
Strategies for using multi-line subscriptions
Understanding the mechanics is one thing, but knowing how to apply this feature effectively requires thinking through your monetization strategy. Let me share several approaches that can help you maximize the value of Subscription with Add-ons.
The modular feature strategy
One of the most straightforward applications is offering a modular subscription where users can customize their plan by selecting only the features they need. Instead of creating multiple pre-defined tiers (Basic, Standard, Premium), you create a base subscription and a menu of add-ons that users can mix and match.
For example, a productivity application might offer a base subscription that includes core features like task management and basic collaboration. Add-ons could include advanced reporting, team management features, third-party integrations, or increased storage. Users who only need advanced reporting can add just that feature, while power users can add multiple add-ons. This approach can increase conversion rates because users feel they are paying only for what they actually need.
The key to making this strategy work is ensuring your base subscription provides genuine value on its own. If the base feels stripped down or incomplete, users may perceive the add-ons as nickel-and-diming rather than genuine flexibility.
The premium upgrade strategy
Another approach uses add-ons as a path to gradually upgrade users over time. You start users on a base subscription and then offer add-ons as upsells based on their usage patterns or at strategic moments in their journey.
Consider a photo editing application where the base subscription includes standard editing tools. As users become more engaged, you can offer add-ons like professional presets, advanced retouching tools, or cloud storage for their edited photos. The advantage of using add-ons rather than a traditional tier upgrade is that users keep their existing features and simply add new capabilities. There is no perception of ‘losing’ their current plan.
This strategy works particularly well when combined with personalized recommendations. By analyzing user behavior, you can surface relevant add-ons at moments when users are most likely to see value in them.
The bundle and save strategy
While Subscription with Add-ons allows individual selection of features, you can also use it to create attractive bundles. By setting prices strategically, you can make bundles of add-ons more appealing than purchasing items individually.
For instance, if your base subscription is $9.99/month and you have three add-ons each priced at $4.99/month, you might offer all three add-ons as a bundle for $11.99/month instead of $14.97/month if purchased separately. Users who want multiple features get a better deal, and you increase your average revenue per user. The bundled price still provides more revenue than users who might only purchase one add-on.
Managing complexity
One risk with add-ons is creating too much complexity. If users face a bewildering array of options, they may experience decision paralysis and abandon the purchase entirely. Consider limiting the number of add-ons to a manageable set, typically three to five options. Provide clear descriptions of what each add-on includes and who it is best suited for. You might also consider offering recommended bundles or a ‘select all’ option for users who want the complete experience.
Implementing Subscription with Add-ons without RevenueCat
Let us walk through how to implement Subscription with Add-ons using the Google Play Billing Library directly. This implementation requires Play Billing Library v5 or higher, though you should use the latest version (currently v8) to ensure access to all features and security updates.
Setting up your products in Google Play Console
Before writing any code, you need to configure your subscription products in the Google Play Console. The good news is that existing subscription products can be offered as add-ons without any special configuration. You do not need to create separate ‘add-on’ type products. Any auto-renewing subscription can serve as either a base item or an add-on.
When creating your products, remember that all items you plan to bundle together must have matching billing periods. If you want to offer both monthly and annual options, you will need to create separate base plans for each billing period.
Querying product details
The first step in your implementation is querying the available products from Google Play. You use the queryProductDetailsAsync method to fetch details for all the subscription products you want to offer.
1class BillingManager(private val context: Context) {
2 private lateinit var billingClient: BillingClient
3
4 private val productIds = listOf(
5 "premium_base_monthly",
6 "hifi_addon_monthly",
7 "offline_addon_monthly",
8 "family_addon_monthly"
9 )
10
11 fun initialize() {
12 billingClient = BillingClient.newBuilder(context)
13 .setListener { billingResult, purchases ->
14 handlePurchasesUpdated(billingResult, purchases)
15 }
16 .enablePendingPurchases()
17 .build()
18
19 billingClient.startConnection(object : BillingClientStateListener {
20 override fun onBillingSetupFinished(billingResult: BillingResult) {
21 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
22 querySubscriptionProducts()
23 }
24 }
25
26 override fun onBillingServiceDisconnected() {
27// Implement retry logic here
28 }
29 })
30 }
31
32 private fun querySubscriptionProducts() {
33 val productList = productIds.map { productId ->
34 QueryProductDetailsParams.Product.newBuilder()
35 .setProductId(productId)
36 .setProductType(BillingClient.ProductType.SUBS)
37 .build()
38 }
39
40 val params = QueryProductDetailsParams.newBuilder()
41 .setProductList(productList)
42 .build()
43
44 billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
45 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
46// Store product details for later use when launching purchase flow
47 handleProductDetails(productDetailsList)
48 }
49 }
50 }
51}
The query returns ProductDetails objects for each subscription product. These objects contain all the information you need to display pricing to users and to launch the purchase flow, including the available base plans, offers, and their associated offer tokens.
Launching the purchase flow with multiple items
When a user selects their desired subscription configuration (base plus selected add-ons), you launch the billing flow with multiple ProductDetailsParams objects. The critical detail here is that the first item in the list becomes the base item, so you must ensure your base subscription is added first.
1fun launchSubscriptionWithAddons(
2 activity: Activity,
3 baseProductDetails: ProductDetails,
4 baseOfferToken: String,
5 addonProductDetailsList: List<Pair<ProductDetails, String>>
6) {
7 val productDetailsParamsList = mutableListOf<BillingFlowParams.ProductDetailsParams>()
8
9// Add the base subscription first - this is crucialval baseParams = BillingFlowParams.ProductDetailsParams.newBuilder()
10 .setProductDetails(baseProductDetails)
11 .setOfferToken(baseOfferToken)
12 .build()
13 productDetailsParamsList.add(baseParams)
14
15// Add each selected add-onfor ((addonDetails, offerToken) in addonProductDetailsList) {
16 val addonParams = BillingFlowParams.ProductDetailsParams.newBuilder()
17 .setProductDetails(addonDetails)
18 .setOfferToken(offerToken)
19 .build()
20 productDetailsParamsList.add(addonParams)
21 }
22
23 val billingFlowParams = BillingFlowParams.newBuilder()
24 .setProductDetailsParamsList(productDetailsParamsList)
25 .build()
26
27 val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)
28
29 if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
30// Handle error - perhaps show a message to the user
31 handleBillingError(billingResult)
32 }
33}
Each item in the list must be unique. You cannot include two ProductDetailsParams objects with the same product ID. The offer token specifies which base plan or offer to use for that item, and you must provide a valid offer token obtained from the ProductDetails.subscriptionOfferDetails() method.
Processing the purchase
After the user completes the purchase flow, your PurchasesUpdatedListener receives the result. Processing a Subscription with Add-ons is similar to processing a single subscription, with one key difference: the purchase grants entitlements for multiple items.
1private fun handlePurchasesUpdated(
2 billingResult: BillingResult,
3 purchases: List<Purchase>?
4) {
5 when (billingResult.responseCode) {
6 BillingClient.BillingResponseCode.OK -> {
7 purchases?.forEach { purchase ->
8 processPurchase(purchase)
9 }
10 }
11 BillingClient.BillingResponseCode.USER_CANCELED -> {
12// User canceled the purchase flow
13 }
14 else -> {
15// Handle other error codes
16 }
17 }
18}
19
20private fun processPurchase(purchase: Purchase) {
21// For Subscription with Add-ons, getProducts() returns all product IDsval purchasedProductIds = purchase.products
22
23// Verify the purchase with your backend server
24 verifyPurchaseWithBackend(purchase) { isValid ->
25 if (isValid) {
26// Grant entitlements for all purchased productsfor (productId in purchasedProductIds) {
27 grantEntitlement(productId)
28 }
29
30// Acknowledge the purchase if not already acknowledgedif (!purchase.isAcknowledged) {
31 acknowledgePurchase(purchase)
32 }
33 }
34 }
35}
36
37private fun acknowledgePurchase(purchase: Purchase) {
38 val params = AcknowledgePurchaseParams.newBuilder()
39 .setPurchaseToken(purchase.purchaseToken)
40 .build()
41
42 billingClient.acknowledgePurchase(params) { billingResult ->
43 if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
44// Handle acknowledgment failure
45 }
46 }
47}
The purchase.products property returns a list of all product IDs included in the purchase. You should grant entitlements for each product and verify the purchase with your backend server before doing so.
Modifying an existing Subscription with Add-ons
Users may want to add more add-ons to their existing subscription or remove some they no longer need. When modifying an existing Subscription with Add-ons, you need to include the current purchase token and specify a replacement mode.
1fun modifySubscriptionAddons(
2 activity: Activity,
3 currentPurchaseToken: String,
4 baseProductDetails: ProductDetails,
5 baseOfferToken: String,
6 newAddonsList: List<Pair<ProductDetails, String>>
7) {
8 val productDetailsParamsList = mutableListOf<BillingFlowParams.ProductDetailsParams>()
9
10// Include the base subscription
11 productDetailsParamsList.add(
12 BillingFlowParams.ProductDetailsParams.newBuilder()
13 .setProductDetails(baseProductDetails)
14 .setOfferToken(baseOfferToken)
15 .build()
16 )
17
18// Include all add-ons (both existing ones to keep and new ones to add)for ((addonDetails, offerToken) in newAddonsList) {
19 productDetailsParamsList.add(
20 BillingFlowParams.ProductDetailsParams.newBuilder()
21 .setProductDetails(addonDetails)
22 .setOfferToken(offerToken)
23 .build()
24 )
25 }
26
27// Configure the subscription updateval subscriptionUpdateParams = BillingFlowParams.SubscriptionUpdateParams.newBuilder()
28 .setOldPurchaseToken(currentPurchaseToken)
29 .setSubscriptionReplacementMode(
30 BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE
31 )
32 .build()
33
34 val billingFlowParams = BillingFlowParams.newBuilder()
35 .setProductDetailsParamsList(productDetailsParamsList)
36 .setSubscriptionUpdateParams(subscriptionUpdateParams)
37 .build()
38
39 billingClient.launchBillingFlow(activity, billingFlowParams)
40}
When modifying a subscription, you must include all items that should remain active after the modification. If you want to keep the base subscription and an existing add-on while adding a new add-on, all three must be in the product list. If you omit an existing add-on from the list, it will be removed.
The replacement mode determines how Google Play handles the billing transition. CHARGE_PRORATED_PRICE calculates a prorated charge for new items to align their renewal with the base subscription. Other modes like CHARGE_FULL_PRICE or WITHOUT_PRORATION are available depending on your use case.
Server-side verification and real-time notifications
For production applications, you must verify purchases on your backend server and handle Real-Time Developer Notifications (RTDN) to keep your entitlement records synchronized with Google Play.
One important detail for Subscription with Add-ons: the subscriptionId field is not provided in RTDN messages for multi-item purchases because there are multiple subscriptions involved. Instead, you should use the purchaseToken from the notification to query the Google Play Developer API and retrieve the full list of entitled items.
1// This is for your backend serverfun handleRealTimeNotification(notification: DeveloperNotification) {
2 val purchaseToken = notification.subscriptionNotification.purchaseToken
3
4// Query the Google Play Developer API for full purchase detailsval subscriptionPurchase = playDeveloperApi
5 .purchases()
6 .subscriptionsv2()
7 .get(packageName, purchaseToken)
8 .execute()
9
10// The lineItems field contains all items in the subscriptionval lineItems = subscriptionPurchase.lineItems
11
12 for (item in lineItems) {
13 val productId = item.productId
14 val expiryTime = item.expiryTime
15 val autoRenewingPlan = item.autoRenewingPlan
16
17// Update your entitlement database based on each item's status
18 updateEntitlement(purchaseToken, productId, expiryTime, autoRenewingPlan)
19 }
20}
The lineItems list in the API response contains details for each subscription item, including its product ID, expiration time, and renewal status. This allows you to track entitlements for each component of the subscription bundle independently.
Handling edge cases
Several edge cases require special attention when implementing Subscription with Add-ons.
Grace periods and account holds apply to the entire subscription bundle. If a renewal payment fails, all items enter the recovery period together, regardless of which specific items were involved in the failed renewal. The grace period duration is determined by the item with the minimum grace period setting among all active items.
Refunds and revocation, interestingly, can be handled at the item level. Using the Google Play Developer API, you can revoke individual items without affecting the entire subscription. This is useful if a user requests a refund for a specific add-on while wanting to keep their base subscription.
Price changes follow similar rules as single-item subscriptions, but with additional complexity when multiple items have pending price increases. All outstanding opt-in price increases must result in the same renewal time with the new price. If an item has a pending opt-in price increase that the user has not confirmed, new price increases for other items in the bundle may be ignored unless they align.
Simplifying Implementation with RevenueCat
While implementing Subscription with Add-ons directly with the Play Billing Library gives you complete control, it also requires managing significant complexity, and time resources (this is a thing) as well. You need to handle billing client lifecycle, query caching, purchase verification, entitlement management, real-time notifications, and all the edge cases we discussed. This is where RevenueCat provides substantial value.
RevenueCat abstracts away the complexities of the Play Billing Library and provides a unified API for managing subscriptions across platforms. For Subscription with Add-ons specifically, RevenueCat handles the intricacies of multi-item purchases, entitlement tracking, and server-side verification automatically.
Configuring Products in RevenueCat
In RevenueCat, you configure your subscription products in the dashboard by creating Products and organizing them into Offerings. Each Google Play subscription product ID maps to a RevenueCat Product, and you can group related products into Offerings that represent your subscription tiers or bundles.
For Subscription with Add-ons, you would create separate products for your base subscription and each add-on. RevenueCat’s entitlement system then allows you to map these products to specific features in your app. When a user makes a purchase, RevenueCat automatically tracks which entitlements they have based on their active subscriptions.
Simplified Purchase Flow
With RevenueCat, launching a purchase and handling the result is significantly simpler. The SDK manages the billing client connection, purchase verification, and entitlement updates.
1class SubscriptionManager(private val context: Context) {
2
3 fun initialize() {
4 Purchases.configure(
5 PurchasesConfiguration.Builder(context, "your_revenuecat_api_key")
6 .build()
7 )
8 }
9
10 suspend fun getAvailableOfferings(): Offerings {
11 return Purchases.sharedInstance.awaitOfferings()
12 }
13
14 suspend fun purchaseSubscriptionWithAddons(
15 activity: Activity,
16 basePackage: Package,
17 addonPackages: List<Package>
18 ) {
19// RevenueCat handles the complexity of bundling these into a single purchaseval purchaseParams = PurchaseParams.Builder(activity, basePackage)
20 .build()
21
22 try {
23 val (transaction, customerInfo) = Purchases.sharedInstance
24 .awaitPurchase(purchaseParams)
25
26// CustomerInfo automatically reflects all active entitlements
27 updateUIWithEntitlements(customerInfo)
28 } catch (e: PurchasesException) {
29 handlePurchaseError(e)
30 }
31 }
32
33 fun checkEntitlements() {
34 Purchases.sharedInstance.getCustomerInfoWith { customerInfo ->
35// Check which entitlements are activeval hasPremium = customerInfo.entitlements["premium"]?.isActive == true
36 val hasHiFi = customerInfo.entitlements["hifi"]?.isActive == true
37 val hasOffline = customerInfo.entitlements["offline"]?.isActive == true
38
39 updateFeatureAccess(hasPremium, hasHiFi, hasOffline)
40 }
41 }
42}
That’s all! Sounds easy? RevenueCat’s CustomerInfo object provides a real-time view of the user’s active entitlements across all their purchases. You do not need to manually track which products map to which features or handle the complexity of multi-item purchases. The SDK and RevenueCat’s backend handle purchase verification, receipt validation, and entitlement calculation automatically.
Cross-platform consistency
One of RevenueCat’s strongest advantages is providing a consistent API across Android, iOS, and other platforms. If your application is available on multiple platforms, RevenueCat ensures that users have a unified subscription experience. A user who subscribes on Android and later switches to iOS will have their entitlements recognized automatically.
Analytics and Insights
RevenueCat’s dashboard provides detailed analytics about your subscription performance, including metrics specific to subscription bundles. You can track which add-on combinations are most popular, monitor conversion rates for different offerings, and identify opportunities to optimize your pricing strategy.
Webhooks and server integration
For applications that need server-side awareness of subscription status, RevenueCat provides webhooks that notify your server of subscription events. These webhooks are simpler to work with than Google Play’s RTDN because RevenueCat normalizes the data and handles the complexity of multi-item purchases.For detailed implementation guidance, refer to the RevenueCat documentation for Google Play products and the subscription management guide.
Wrapping up
Subscription with add-ons gives us values in how Android developers can structure their subscription offerings. By enabling multi-item purchases with synchronized billing, Google has opened new possibilities for flexible, user-friendly monetization strategies. Whether you choose to offer modular subscriptions where users customize their plan, use add-ons as an upgrade path for engaged users, or create attractive bundles that increase average revenue per user, this feature provides the tools to implement your vision.
Implementing this feature directly with the Play Billing Library requires careful attention to product configuration, purchase flow management, and edge case handling. The code examples in this article provide a foundation, but production implementations will need additional error handling, retry logic, and thorough testing.
For teams that want to move faster or need cross-platform support, RevenueCat offers a compelling alternative that abstracts away much of this complexity while providing additional benefits like unified analytics and simplified server integration.
As you plan your implementation, start by clearly defining your monetization strategy. Understand which features should be in your base subscription versus add-ons, ensure your billing periods align, and consider how you will communicate the value of each option to users. With thoughtful planning and solid implementation, Subscription with Add-ons can help you create a subscription experience that serves both your users and your business goals.For the complete official documentation on this feature, visit the Android Developers guide for Subscription with Add-ons, and RevenueCat Offerings.

