How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/pollyglot/google-play-cli-skills --skill gplay-monetizationBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
gplay monetization (subscriptions + one-time products)
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.
The loop
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.
Mirror semantics: the trap if you know metadata
metadata 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:
- Never point `--dir` at a fresh/empty directory.
applyrefuses when the
directory 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
.jsonfiles so the
directory 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.
- Deleting a subscription is additionally guarded server-side: Google refuses
to delete one with a published base plan.
Gates
| 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 | n/a |
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.
Subscriptions
gplay 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- Base plans ride the parent patch. Base plan config (billing type,
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.
- Offers are embedded but real.
pullnests each offer under
basePlans[].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.
- Reconciled fields only.
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).
Prices
# Derive per-region prices from one base price, a computation, no write:
gplay subscriptions prices convert --price 4.99 --currency USD --output jsonPaste 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.
# 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 … --confirmThis 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.
- Cohorts older than `--oldest` (RFC-3339) migrate, scoped to the
--regions you repeat.
--price-increase-type opt-inrequires subscribers to accept or churn;
opt-out (where Google allows it) applies automatically with notice.
- No bulk migration, the batch sibling is deliberately not wrapped. One
base plan per invocation.
One-time products (iap)
gplay 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-version
create/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.
CI gate
gplay subscriptions apply --dry-run --output json # the plan, a gplay-owned shape
gplay iap apply --dry-run --output jsonapply --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:
gplay subscriptions apply --dry-run --output json | jq -e '.changes | length == 0'Permissions
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).



