Chapter 17: Managing Your Product Catalog at Scale
Your product catalog is the foundation of your monetization strategy. Every subscription, one time product, base plan, and offer starts as an entry in that catalog. When your catalog contains a handful of products, managing it through the Play Console works fine. But as your app grows and you add regional pricing, seasonal promotions, multiple subscription tiers, and localized listings, the console becomes a bottleneck. You need automation.
Google provides a comprehensive set of server side APIs for managing your product catalog programmatically. This chapter covers those APIs in depth: how to create and update products in bulk, how to stay within quota limits, how to keep your local database in sync with Google Play, and how to handle the full lifecycle of a product from creation through deprecation.
Catalog Management APIs
The Google Play Developer API exposes two primary sets of endpoints for catalog management, split by product type.
One Time Products
One time products have two available API surfaces. The legacy inappproducts service and the newer monetization.onetimeproducts service. The newer service supports the expanded one time product object model, which includes multiple purchase options and offers per product. If you are starting fresh, use the monetization.onetimeproducts endpoints. If you have an existing catalog built on inappproducts, you can migrate products to the new model by sending an update through the onetimeproducts service. Once migrated, a product can no longer be accessed through the old inappproducts service.
The monetization.onetimeproducts service provides these methods:
createcreates a new one time productpatchupdates specific fields on an existing productgetretrieves a single product by its product IDlistretrieves all one time products for your appbatchGetretrieves multiple products by their IDs in one callbatchUpdatecreates or updates up to 100 products in a single requestbatchDeletedeletes up to 100 products in a single requestdeletedeletes a single product
The legacy inappproducts service mirrors most of this functionality with insert, update, patch, get, list, batchGet, batchUpdate, batchDelete, and delete. It remains functional for backward compatibility, but new features only ship on the monetization.onetimeproducts surface.
Subscriptions
Subscription management lives under monetization.subscriptions and is organized into three layers that match the subscription product hierarchy you learned in Chapter 4.
Subscription level methods manage the top level container:
create,patch,get,list,batchGet,batchUpdate,delete
Base Plan level methods manage billing terms within a subscription:
activate,deactivate,delete,batchUpdateStatesmigratePricesmigrates subscribers to the latest price version
Offer level methods manage promotional pricing attached to base plans:
create,patch,get,list,batchGet,batchUpdateactivate,deactivate,batchUpdateStates
This three layer structure means creating a fully configured subscription requires multiple steps. You create the subscription container first, then add base plans, then optionally attach offers to those base plans. The batchUpdate method at the subscription level can handle all of this in a single call when you include nested base plans and offers in the request body.
Batch Methods
Every product endpoint provides batch methods that accept up to 100 individual operations per request. This is the primary tool for managing catalogs at scale.
For one time products, monetization.onetimeproducts.batchUpdate accepts a list of up to 100 update requests. Each nested request specifies the product to create or update, along with configuration for how that individual update should behave. The batchDelete method works similarly, accepting up to 100 product identifiers to remove.
For subscriptions, monetization.subscriptions.batchUpdate accepts up to 100 subscription update requests. Each nested request can include the full subscription definition with base plans and offers, making it possible to create complete subscription products in bulk.
The batch methods offer two important advantages beyond convenience. First, they count as a single query against the per minute and hourly modification quotas (more on this shortly). A batch request containing 100 updates consumes the same quota as a single update in most cases. Second, they support the latency tolerance parameter, which unlocks higher throughput for large catalog operations.
Here is the general structure of a batch update call for one time products:
If your catalog has more than 100 products, you chunk the list and send multiple batch requests. With latency tolerant mode, you can process thousands of products without hitting quota limits.
Latency Tolerance Options
Every modification method (create, update, patch, delete) supports a latencyTolerance field that controls how quickly your changes propagate to end users. This is a tradeoff between speed and throughput.
Latency sensitive (default). Changes propagate within minutes. Use this when you need updates to be visible quickly, such as fixing an incorrect price or activating a time sensitive promotion. This mode consumes quota from all three quota tiers, including the most restrictive one.
Latency tolerant. Changes may take up to 24 hours to propagate. Use this for bulk operations where immediate visibility is not required, such as initial catalog creation, periodic reconciliation, or large scale price updates across regions. This mode skips the most restrictive quota tier entirely, giving you much higher effective throughput.
You set latency tolerance at the individual request level. In a batch request, each nested operation can specify its own tolerance. This means you can mix latency sensitive and latency tolerant operations in the same batch, though doing so means the batch will consume quota from the sensitive tier for each sensitive operation it contains.
The practical guidance is straightforward. Default to latency tolerant for any bulk operation or background synchronization. Use latency sensitive only when a change must be live quickly, like correcting a pricing error or responding to a compliance issue.
Quota and Rate Limiting
The Google Play Developer API uses a three tier quota system for catalog modification endpoints. Understanding this system is essential for building reliable automation, because hitting quota limits means your updates will fail with HTTP 429 or 403 errors until the quota resets.
Tier 1: Per Minute Query Limit
All API calls, both reads and writes, share a per minute bucket. The default limit is 3,000 queries per minute for the publishing and monetization bucket. A single request counts as one query. A batch request also counts as one query, regardless of how many individual operations it contains. This means a batch of 100 updates costs the same as a single update against this tier.
Tier 2: Hourly Modification Limit
All product modification operations (create, update, patch, delete) share a single hourly limit of 7,200 queries per hour. This limit spans both one time products and subscriptions. A batch modification call counts as one query against this tier, regardless of the number of nested operations or their latency sensitivity. This is where batch methods provide their biggest advantage. Processing 7,200 individual updates would consume your entire hourly modification budget. Processing 72 batch requests of 100 items each achieves the same result while using only 72 queries of your 7,200 hourly limit.
Tier 3: Hourly Latency Sensitive Modification Limit
Latency sensitive modifications have an additional hourly limit of 7,200 per hour. Unlike Tier 2, this limit counts individual operations within a batch request. A single batch request containing 100 latency sensitive updates consumes 1 query from Tier 1, 1 query from Tier 2, and 100 queries from Tier 3. The same batch request with latency tolerant mode consumes 1 query from Tier 1, 1 query from Tier 2, and 0 queries from Tier 3.
This tiered system creates a clear incentive structure. Batch methods with latency tolerant mode give you the highest throughput. Here is how the math works for different approaches:
The last row is the sweet spot. Batches of 100 with latency tolerant mode let you modify up to 720,000 products per hour while staying within all quota limits. For most catalogs, this is more than enough.
If you hit quota limits, the API returns an error response:
Handle these errors with exponential backoff. Monitor your quota usage through the Google Cloud Console's Quotas section. If your default limits are insufficient, you can request an increase through Google Play's support portal.
Initial Catalog Creation Strategies
When you first set up programmatic catalog management, you need to populate Google Play with your entire product catalog. The strategy depends on catalog size.
Small Catalogs (Under 100 Products)
For small catalogs, you can use individual create calls or a single batch request. The monetization.subscriptions.create and monetization.onetimeproducts.create methods work well here. There is no need for latency tolerance optimization when you are creating fewer than 100 products.
For subscriptions, remember that base plans created through the API start in a DRAFT state. You must explicitly activate each base plan using monetization.subscriptions.basePlans.activate before users can purchase them. This is a common source of confusion. You create the subscription with its base plans, but nothing is available for purchase until you activate at least one base plan.
Medium Catalogs (100 to 10,000 Products)
Use batchUpdate with allowMissing set to true and latency tolerance set to PRODUCT_UPDATE_LATENCY_TOLERANCE_LATENCY_TOLERANT. The allowMissing flag tells the API to create the product if it does not exist, making the same call work for both creation and updates. Chunk your catalog into groups of 100 and send batch requests sequentially.
At 100 products per batch and 7,200 batches per hour allowed against Tier 2, you can create all 10,000 products in under 2 minutes of wall clock time.
Large Catalogs (Over 10,000 Products)
The same batch approach works, but you should add pacing to stay safely within quota limits. Insert a short delay between batch requests if you are processing tens of thousands of products. Monitor your quota consumption and adjust the delay dynamically. For extremely large catalogs (100,000+ products), spread the initial creation over multiple hours or schedule it during off peak hours.
Also consider running a dry run first. Before sending creation requests, validate your product data locally. Check for duplicate product IDs, missing required fields (like listings and pricing), and invalid values. Catching errors before they hit the API saves both quota and debugging time.
Product Updates: patch, update, and batchUpdate
Once your catalog exists, you will need to update it. Prices change, listings get new translations, tax categories shift, and new offers launch. The API provides three distinct update mechanisms, each suited to different use cases.
patch
The patch method updates specific fields on an existing product without touching other fields. You specify which fields to modify using an updateMask parameter, a comma separated list of field paths. Only the fields listed in the mask are modified. Everything else stays the same.
Use patch when you are modifying a known set of fields and want to avoid accidentally overwriting other configuration. This is the safest update method for targeted changes.
For subscriptions, you must include regionsVersion in the request body when patching regional pricing. This version field ensures you are working with the correct region configuration and prevents conflicts.
update (inappproducts only)
The update method on the inappproducts endpoint replaces the entire product resource. You send a complete product definition, and it overwrites whatever was there before. It also supports allowMissing, which creates the product if it does not exist, making it an upsert operation.
This method is useful when your source of truth is a complete product definition, and you want to ensure Google Play matches it exactly. The risk is that any field you omit from the request body gets cleared or reset to defaults. Always send the full product definition when using update.
batchUpdate
The batchUpdate method combines batch processing with the update or patch behavior. Each nested request in the batch specifies its own update configuration. You can mix creates and updates in the same batch using the allowMissing flag.
For subscriptions, batchUpdate is particularly powerful because each nested request can include the full subscription hierarchy: the subscription container, its base plans, and attached offers. This means you can update a complex subscription product, including all its plans and offers, in a single nested request within a larger batch.
The general rule: use patch for targeted field updates on individual products, and use batchUpdate for bulk operations where you are modifying many products at once.
Catalog Reconciliation and Diff Synchronization
If you manage your catalog through an internal CMS, admin panel, or configuration system, you need a way to ensure Google Play's catalog matches your local source of truth. This process is called catalog reconciliation.
The core idea is simple: periodically read the full catalog from Google Play, compare it against your local catalog, compute the differences, and apply the necessary changes. In practice, there are several details to get right.
Step 1: Fetch the Remote Catalog
Use the list endpoints to retrieve every product from Google Play. For one time products, call monetization.onetimeproducts.list (or inappproducts.list for legacy products). For subscriptions, call monetization.subscriptions.list. Both endpoints support pagination. Keep fetching pages until you have all products.
Step 2: Build a Diff
Compare each product in your local catalog against the remote catalog. You are looking for three types of differences:
- Missing remotely. Products that exist in your local catalog but not on Google Play. These need to be created.
- Missing locally. Products that exist on Google Play but not in your local catalog. These may need to be deactivated or deleted, depending on your policy.
- Divergent. Products that exist in both places but with different values. These need to be updated.
For the divergent case, compare the specific fields you care about: pricing, listings, tax settings, offer configuration. You do not need to compare every field. Focus on the ones your system manages.
Step 3: Apply Changes
Group your changes into batch requests. Create missing products using batchUpdate with allowMissing: true. Update divergent products using batchUpdate with the appropriate updateMask. Handle deletions or deactivations separately.
Scheduling and Safety
Run reconciliation on a schedule that fits your update frequency. Daily reconciliation works well for most apps. If your catalog changes frequently throughout the day, run it more often.
Always log the diff before applying changes. In a large catalog, a bug in your diff logic could trigger mass deletions or overwrites. Build in safeguards: if the diff shows more than a certain percentage of products changing, flag it for manual review instead of applying automatically. A reconciliation run that wants to delete 50% of your catalog is almost certainly a bug, not a legitimate change.
Use latency tolerant mode for reconciliation. The changes do not need to propagate immediately, and the higher throughput keeps reconciliation fast even for large catalogs.
Product Deprecation and Deletion
Products have a lifecycle. At some point, you may need to retire a product because a feature is being discontinued, you are consolidating subscription tiers, or a product was created by mistake. The approach differs between one time products and subscriptions.
One Time Products
One time products can be deleted outright using monetization.onetimeproducts.delete or monetization.onetimeproducts.batchDelete. Deletion removes the product from your catalog permanently. Existing purchases are not affected, as users who already bought the product retain their entitlement. But the product will no longer appear in purchase flows or product queries.
Before deleting, consider whether any users might still need to restore their purchase on a new device. If your app checks the product catalog during purchase restoration, a deleted product could cause issues. A safer approach for many apps is to keep the product in the catalog but remove it from your storefront so new purchases cannot happen while existing entitlements remain restorable.
Subscriptions
Subscriptions are more constrained. You can only delete a subscription if no base plan has ever been activated. Once any base plan has been activated, even if you later deactivated it, the subscription cannot be deleted. This restriction exists because active subscribers may hold entitlements tied to that subscription, and deleting it would create inconsistencies.
For subscriptions you want to retire, the approach is to deactivate all base plans. Deactivated base plans stop accepting new subscribers but continue serving existing ones through their current billing cycle. Existing subscribers can still renew, but the plan is no longer visible to new users.
For base plans still in DRAFT state (never activated), you can delete them using monetization.subscriptions.basePlans.delete. This is useful for cleaning up test configurations or correcting mistakes before going live.
The practical workflow for sunsetting a subscription:
- Deactivate all base plans on the subscription.
- Remove the subscription from your app's storefront and purchase flows.
- Continue processing renewals for existing subscribers until they churn naturally.
- Monitor active subscriber counts. When all subscribers have churned, the subscription is effectively retired.
Do not try to force migrate subscribers by deleting their subscription product. Instead, build migration flows in your app that encourage users to switch to the replacement product on their own terms.
Offers and Promotions
Beyond the standard catalog management APIs, Google Play provides a separate promotion system for distributing products and subscription trials through redeemable codes. This is useful for marketing campaigns, partnership deals, influencer programs, and customer support gestures.
One Time Use Codes
One time use codes are unique, auto generated codes that can each be redeemed once. You create them through the Play Console (not through the API). Each code is a distinct string that grants the user access to a one time product or a subscription free trial.
The quarterly limits depend on the product type:
- One time products: 500 codes per quarter across all managed products in your app. This is a shared pool. If you have 10 one time products, the 500 limit applies to all of them combined.
- Subscriptions: 10,000 codes per quarter per subscription product. Each subscription product gets its own independent limit.
Unused codes do not carry over to the next quarter. If you generate 500 codes for one time products and only distribute 200, the remaining 300 expire when the quarter ends. Plan your campaigns accordingly.
Users can redeem one time use codes either through the Google Play Store directly or within your app. When redeemed through the Play Store, the user is prompted to open your app (if installed) or download it. When redeemed in app, the purchase flows through your normal Play Billing Library integration.
Custom Codes
Custom codes are codes that you specify rather than having Google auto generate them. Unlike one time use codes, a single custom code can be redeemed multiple times up to a limit you set during creation.
Important restrictions on custom codes:
- Available only for subscriptions, not one time products.
- Only redeemable by users who have never previously subscribed to that product. This means custom codes are an acquisition tool, not a retention tool.
- Redemption limit must be between 2,000 and 99,999 per code.
- Users can only redeem custom codes in app, not through the Play Store.
Custom codes work well for partnership promotions where you want to give a specific, branded code (like "PARTNER2026") to a large audience. The redemption limit ensures you control how many users can take advantage of the promotion.
Deep Link Redemption
You can create a deep link that takes users directly to the Google Play redemption screen with the code pre populated:
When a user opens this link, the Play Store opens with the code field already filled in. If the user has the latest version of your app installed, they are prompted to open it after redemption. If not, they are prompted to download or update the app first.
This deep link format works in emails, push notifications, social media posts, QR codes on physical media, and anywhere else you can share a URL. It reduces friction compared to asking users to manually navigate to the Play Store and type in a code.
Creating Promotions in Practice
All promotion creation happens through the Play Console, not the API. Navigate to your app's Monetize section, select Promotions, and create a new promotion. You choose the promotion type (one time use or custom), select the product or subscription, set the promotion duration (up to one year), and specify the number of codes.
Once created, you can download the generated codes as a CSV file for one time use codes, or note the custom code string for distribution. The codes become active immediately unless you set a future start date.
A few practical tips:
- Track redemption rates. The Play Console shows how many codes have been redeemed, but integrate this with your own analytics to understand the downstream conversion and retention of promo code users.
- Set expiration dates. Every promotion should have an end date. Open ended promotions are hard to forecast and can create support issues when users try to redeem expired codes.
- Test codes before distribution. Generate a small batch first, redeem one yourself, and verify the full flow works correctly in your app before sending codes to thousands of users.
- Plan around quarterly resets. If you have a major campaign planned, ensure you have enough quota in the current quarter. The 500 per quarter limit for one time products is particularly tight for large campaigns.