# Product sets and packages

A product set is the ordered purchase presentation for an app. A package is a slot in that set that names a store product. The application reads the current set, loads those products from StoreKit or Google Play, and displays the store's localized price and offer information.

## Create a product set

Open **Product sets → New product set**. Enter a stable identifier such as `default` and a display name. Open the new set and add its packages.

| Package field | Meaning |
| --- | --- |
| Identifier | Unique slot within the set, such as `monthly_ios` |
| Package type | `custom`, `lifetime`, `annual`, `six_month`, `three_month`, `two_month`, `monthly`, or `weekly` |
| Platform | `apple` or `google` |
| Product ID | The exact SKU for that app and store |
| Position | Unique integer from 0 to 1000; determines ascending order |

Identifiers and positions must be unique within a set, including across stores. For a cross-platform set, use distinct package identifiers such as `monthly_ios` and `monthly_android` rather than duplicating a slot name.

## Make a set current

Mark the intended set as the app's current product set. There is one current presentation for the app, not one per user. Editing or selecting it changes what subsequent runtime reads return. Existing purchases and access mappings are independent of this selection.

The runtime read requires the publishable key plus customer token for a public client, or a secret key on a trusted server:

```sh
curl -fsS "$CHERRYSUB_API_URL/v1/product-sets/current" \
  -H "Authorization: Bearer $CHERRYSUB_PUBLISHABLE_KEY" \
  -H "X-IAP-Customer-Token: $CHERRYSUB_CUSTOMER_TOKEN"
```

The body contains `product_set`, either an object with ordered `packages` or explicit `null`. A not-yet-configured app is not a transport error.

## Use the set in your storefront

1. Fetch the current set after authenticating your application user.
2. Keep packages for the platform being shown, in the returned order.
3. Load those product IDs using StoreKit or Google Play Billing.
4. Display storefront-provided title, localized price, eligibility, and offers.
5. On purchase, submit the verified purchase evidence with the same authenticated App User ID.

Package type is presentation metadata; it does not create the store's billing period or base plan. The set does not contain a trustworthy locally computed price or a hosted paywall layout. Your application owns the purchase screen and offer choice.

## Empty, invalid, and stale presentations

If `product_set` is `null`, show a deliberate unavailable/empty state and investigate configuration. If the current set has no package for the device's platform, do not present a different store's product. If the store cannot load a configured SKU, verify app identity, product availability, agreements, and tester eligibility.

The SDKs fail closed on malformed, duplicated, or out-of-order package data. They do not silently reorder invalid input or substitute another set. Current product-set responses use `Cache-Control: no-store`; apply an explicit refresh strategy if your app keeps its own UI cache.

## Editing and removal

Add/remove packages and update the set's name/current selection from its detail page. Review the result on both platforms before release. Removing a package or a product set does not cancel subscriptions or remove access granted by separate mappings.

The current version does not include audience-targeted sets, remote paywall layout editing, or A/B assignment. Use [release status](/docs/limits-and-release-status) to distinguish available runtime presentation from planned features.

## Imported packages and Google base plans

RevenueCat offerings can be imported as product sets, with packages split into one entry per store. The import report shows identifier transformations, such as `$rc_monthly` to `rc_monthly_google`. An existing current set stays selected unless activation is requested explicitly.

Imported Google packages can carry `base_plan_id`. The SDK exposes it as `basePlanId` (Kotlin, Dart, TypeScript), `basePlanID` (Swift), or `BasePlanID` (Go). It is optional and absent for Apple packages. Preserve it when resolving the Google subscription offer token; the subscription product ID alone can identify several durations or plans. An imported product's base plan list is catalog metadata and is not a localized price list.

When using a native Google Billing `ProductDetails`, select a subscription offer whose `basePlanId` matches the imported package, then apply your application's offer eligibility policy to choose the token. Do not pick the first offer across all base plans. The current manual package form has no base-plan field; use a portable JSON import for an exact Google base-plan package. CherrySub access mappings remain product-level; imports that would combine different base-plan entitlements are blocked. [Migration](/docs/migration).
