Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Manage the monetization catalog — subscriptions and one-time products — as files with gplay `subscriptions` and `iap`; the directory is a mirror, so a live product with no file is a delete. Use when putting subscriptions or IAP under version control, editing a product's prices/offers/listings, reviewing catalog drift in CI, promoting a legacy in-app product to the v2 model, or migrating live subscribers to a new price.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 107% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -21% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 33% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 98% | 0% |
gplay subscriptions and gplay iap own the monetization catalog as declarative, version-controlled files (ADR-0041). Shared conventions (auth, --package pinning, output, exit codes, --dry-run/--confirm) are in gplay-cli-usage. Both namespaces are [experimental].
| Namespace | What it holds | Default --dir | |---|---|---| | subscriptions | subscriptions, their base plans (config + per-territory prices), their offers, lifecycle state | ./monetization/subscriptions | | iap | one-time products — v2 monetization.onetimeproducts ∪ legacy inappproducts | ./monetization/iap |
Both sit outside the Edit lifecycle (like compliance, device-tiers, recovery, orders) — direct package-scoped writes, no editId, so gplay edits begin does not batch them.
pull → edit the .json files → apply --dry-run → apply [--confirm]One <productId>.json per product, holding the API resource verbatim minus server-derived noise. pull then apply with no edits is a guaranteed no-op. Commit the directory; the diff in review is the catalog change.
metadatametadata apply is additive: a locale live online but absent on disk is left alone. Monetization is the opposite. The directory is the complete declared catalog — a live subscription, product or offer with no file is a delete in the plan. A monetization catalog is a closed set whose omissions must be visible; a store listing tree is a partial view.
Consequences to internalize before running anything:
--dir at a fresh/empty directory. apply refuses when thedirectory holds no .json while the app has live products (it would delete them all) — but a partially populated directory is a legitimate plan full of deletes. Always pull first.
pull is destructive locally too. It removes stale .json files so thedirectory mirrors Play. It refuses to erase a populated directory when the live catalog reads back empty (a mis-set --package or a scope loss) — that refusal is a signal, not an obstacle to work around. Non-.json files are never touched.
to delete one with a published base plan.
| Situation | Gate | Exit without it | |---|---|---| | Plan contains any delete (product or offer) | --confirm | 3 | | iap apply promotes a live legacy product to v2 | --migrate | 3 | | subscriptions prices migrate (reprices live subscribers) | --confirm | 3 | | Creates, patches, state changes | none — they run directly | — |
CI=true never auto-confirms. GPLAY_READONLY refuses every apply and migrate outright (exit 4, not resolvable by adding a flag).
State changes are not gated — activate/deactivate are reversible — but they are listed prominently in every plan view because they move buyer availability.
bashgplay subscriptions pull # → ./monetization/subscriptions/*.json # …edit the files… gplay subscriptions apply --dry-run # ONLINE read, prints the plan, changes nothing gplay subscriptions apply # creates/patches/state changes gplay subscriptions apply --confirm # …when the plan also deletes
per-territory regionalConfigs prices) is declared inline under basePlans and patched with the subscription — the API has no create/patch on the sub-resource. Its endpoints only manage state and subscriber price migration.
pull nests each offer underbasePlans[].offers — a file construct the API resource does not carry. apply splits them back out and reconciles them through the offers endpoints under the key productId/basePlanId/offerId. Don't expect that array in an API response.
state: declares lifecycle, reconciled via :activate/:deactivate,never a patch. Declare ACTIVE or INACTIVE. Omitting the field leaves state unmanaged — the metadata stance. An unreachable transition (DRAFT from anything, INACTIVE from DRAFT) is a usage error naming it.
listings, taxAndComplianceSettings,restrictedPaymentCountries, basePlans. The updateMask is exactly the changed managed fields — nothing outside that projection drifts or diffs. archived is not reconciled (deprecated/output-only upstream).
bash# Derive per-region prices from one base price — a computation, no write: gplay subscriptions prices convert --price 4.99 --currency USD --output json
Paste the returned Money objects into a base plan's regionalConfigs, then rehearse with apply --dry-run. --output json is the ConvertRegionPricesResponse verbatim. "Not a write" does not mean offline: convert calls the convertRegionPrices API (today's exchange rates), so it needs a credential and the package axis — it just never mutates anything.
bash# Reprice EXISTING subscribers — money-moving, one base plan per call: gplay subscriptions prices migrate \ --product premium --base-plan monthly \ --region FR --region DE \ --oldest 2026-01-01T00:00:00Z \ --price-increase-type opt-in \ --dry-run # offline preview, lists the gate in "requires" gplay subscriptions prices migrate … --confirm
This is the one deliberate exception to "editing files never touches a live purchaser." apply changes what new buyers pay; migrate changes what existing subscribers pay. An apply diff never triggers a migration — that separation is pinned by a test upstream, so don't expect a price edit to propagate to current subscribers.
--oldest (RFC-3339) migrate, scoped to the--regions you repeat.
--price-increase-type opt-in requires subscribers to accept or churn;opt-out (where Google allows it) applies automatically with notice.
base plan per invocation.
iap)bashgplay iap pull # v2 ∪ legacy → ./monetization/iap/*.json gplay iap apply --dry-run gplay iap apply [--confirm] [--migrate]
pull unions the v2 and legacy surfaces by product ID, and a file's origin is its shape — no gplay-invented marker:
| Field present | Model | |---|---| | sku | legacy inappproducts | | productId | v2 onetimeproducts |
Legacy is inert: gplay never creates, edits or deletes a legacy product — the only gesture is the one-way promotion to v2 (rewrite the file with productId and apply with --migrate; rehearse with --dry-run first). When a legacy file is involved — an unexpected refusal, a promotion to plan, or the question of why pull reads two surfaces — read iap-legacy.md.
--regions-versioncreate/patch require Google's regions version string. gplay pins the current published value (2022/02) and exposes --regions-version to override when Google publishes a new one — a flag, not a config knob, so the pin stays visible in CI logs.
bashgplay subscriptions apply --dry-run --output json # the plan, a gplay-owned shape gplay iap apply --dry-run --output json
apply --output json emits the plan, not an API echo — a recorded ADR-0003 exception, like metadata apply, [experimental] until it graduates: {package, dryRun, changes[], summary{…}, requires[]}, where each change carries op (create/patch/delete/activate/deactivate, plus migrate on iap) and its identity. pull --output json is the API pass-through (the merged ListSubscriptionsResponse, or the composite {"oneTimeProducts":[…],"inappproduct":[…]}) — but the files are the real output there.
A drift check is one line — fail the job when the plan is non-empty:
bashgplay subscriptions apply --dry-run --output json | jq -e '.changes | length == 0'
The Discovery snapshot ties no specific Play permission enum to the monetization methods, so gplay's 403 hint points at the surface rather than naming a capability: grant the service account access to the app's monetization setup in Play Console (Users & permissions), then retry. 403 → exit 11, 404 on the package → exit 30 (verify --package or the pin).
Confirm the current verbs and flags with gplay subscriptions --help, gplay iap --help, and gplay <group> <command> --help — both surfaces are [experimental] (ADR-0010) and may still evolve.
Other measured skills in the registry, with their headline benchmark lift.