Google Play gives you two ways to structure subscriptions. Pick one before you create products in the console, because changing later is painful.
Each billing period is its own subscription product with its own product ID.
com.yourapp.pro.monthly (subscription, 1 base plan)
com.yourapp.pro.annual (subscription, 1 base plan)
This is the simplest structure and mirrors how subscriptions work in App Store Connect. If you also ship on iOS, your Google product IDs can match your Apple product IDs exactly.
One subscription product contains multiple base plans, one per billing period.
com.yourapp.pro (subscription)
├── monthly (base plan)
└── annual (base plan)
This is Google's recommended pattern when you want users to upgrade or downgrade between billing periods within the same subscription. It enables Google's native upgrade/downgrade flow, where switching from monthly to annual is treated as a plan change instead of a new purchase.
| You want | Use |
|---|---|
| The simplest setup, or matching Apple's structure | Flat products |
| Users to switch between monthly and annual within one subscription | Umbrella + base plans |
| Multiple subscription tiers (e.g., Vendor and Employer) that are independent of each other | One umbrella per tier |
Both patterns work with PurchaseKit. The difference is how you configure products in the dashboard and what comes through in the webhook payload.
com.yourapp.pro.annual)annual), set the billing period and price, then Activatecom.yourapp.pro.annual)PurchaseKit uses the first base plan automatically. The webhook payload sends store_product_id set to the full product ID and google_base_plan_id as null.
com.yourapp.pro)monthly as the base plan ID, configure billing and price, then Activateannual, configure, then ActivateIf you have multiple independent subscription tiers (for example, Vendor and Employer), create one umbrella subscription per tier. Each gets its own product ID and its own set of base plans.
Create one PurchaseKit product per (umbrella, base plan) pair:
| PurchaseKit name | Google product ID | Base plan ID |
|---|---|---|
| Pro Monthly | com.yourapp.pro |
monthly |
| Pro Annual | com.yourapp.pro |
annual |
Both products share the same Google product ID; the base plan ID is what distinguishes them. Without the base plan ID, both products would show the same (first) price and you couldn't tell them apart in your webhook handler.
When a user buys the annual base plan of com.yourapp.pro:
{
"store": "google",
"store_product_id": "com.yourapp.pro",
"google_base_plan_id": "annual",
...
}
store_product_id is the umbrella ID. google_base_plan_id is the base plan the user actually purchased. Together, they identify which PurchaseKit product (and therefore which plan in your Rails app) was bought.
For flat products, google_base_plan_id is null. For Apple subscriptions, it's always null.
If you store plans in your Rails app and want to map the webhook back to a plan record, key your lookup on both fields when google_base_plan_id is present:
plan =
if event.google_base_plan_id.present?
Plan.find_by(
google_product_id: event.store_product_id,
google_base_plan_id: event.google_base_plan_id
)
else
Plan.find_by(apple_product_id: event.store_product_id) ||
Plan.find_by(google_product_id: event.store_product_id)
end
Add a google_base_plan_id column to your plans table if you don't already have one.
When using the Pay gem integration, google_base_plan_id is also persisted on Pay::Subscription#data["google_base_plan_id"] so it's available later without re-fetching from the store.
Configure Google Play Console to send webhooks to PurchaseKit.