Skip to main content

Google Play Console Setup

A tight wrapper around the official Google Play Android Publisher API. All subcommands authenticate via the service account JSON at googleServiceAccountPath. No external CLI and no extra npm dependencies — the JWT flow and HTTPS calls are implemented with Node's built-in crypto and fetch.

note

Google Play does not allow creating new apps via any public API. Create the app manually once in Play Console, then use these commands to configure it.

gpc setup

Full end-to-end configuration (11 steps) — the Google Play parallel to create-appstore-app.

kappmaker gpc setup
kappmaker gpc setup --config ./my-config.json

Alias: kappmaker create-play-app

What it Does (11 Steps)

  1. Validate service account + obtain access token
  2. Load config (./Assets/googleplay-config.json or interactive prompts). Auto-detects Play's actual default language and migrates legacy product IDs
  3. Review summary and confirm
  4. Verify app exists on Play Console (fails fast with a deep link if not)
  5. Start a Play Console edit (skipped if default-language listing has no title)
  6. Update app details (default language + contact website/email/phone)
  7. Update store listings per locale (title, short/full description, video); commits the edit. Empty listing titles are auto-filled from the app name
  8. Create subscriptions via the new monetization API. Base plans are available to all ~175 Play regions (not just the regions with explicit prices — Google auto-converts pricing for the rest). Idempotent — existing product IDs are skipped. Skipped if no build is uploaded to any track
  9. Create one-time in-app products via the new monetization API. Idempotent
  10. Update the data safety declaration (JSON → CSV conversion using bundled canonical template)
  11. Print a checklist of manual-only Play Console declarations that have no REST endpoints

Options

FlagDescriptionDefault
--config <path>Path to JSON config file./Assets/googleplay-config.json

gpc listings push

Push just the store listings section from the config file. Useful after editing copy.

kappmaker gpc listings push
kappmaker gpc listings push --config ./my-config.json

Runs a single edit transaction: updates app details → updates every listing locale → commits.


gpc subscriptions list

Read-only — lists existing subscription product IDs on Play Console.

kappmaker gpc subscriptions list --package com.example.myapp
kappmaker gpc subscriptions list # uses app.package_name from the config file

gpc subscriptions push

Create or reuse subscriptions from the config file. Idempotent — already-existing product IDs are skipped.

kappmaker gpc subscriptions push

:::tip Adding ONE more subscription without editing the config

kappmaker subscription add --period weekly --price 9.99 --platform android pushes a single new subscription with auto-aligned IDs across both stores. See Quick-add Subscription / IAP.

:::

Uses the new monetization API. Auto-generated IDs:

FieldFormatExample ($6.99 weekly)
productId{appname}.premium.{period}.v1myapp.premium.weekly.v1
basePlanIdautorenew-{period}-{priceDigits}-v1autorenew-weekly-699-v1
Subscription title{AppName} Premium {PeriodLabel}MyApp Premium Weekly

gpc iap list

Read-only — lists existing one-time in-app product SKUs on Play Console.

kappmaker gpc iap list --package com.example.myapp
kappmaker gpc iap list

gpc iap push

Create or reuse one-time in-app products from the config file. Idempotent — existing products are PATCHed with fresh PPP regional pricing.

kappmaker gpc iap push
kappmaker gpc iap push --recreate-stuck # DELETE + recreate "stuck" products (see below)

Uses the new monetization API (PATCH /onetimeproducts/{id}?allowMissing=true), replacing the legacy /inappproducts endpoint.

Options

FlagDescriptionDefault
--config <path>Path to JSON config file./Assets/googleplay-config.json
--recreate-stuckDELETE products whose stored regions can't coexist with regionsVersion=2022/02 (e.g. legacy products with MN stored), then recreate fresh. WARNING: Google soft-deletes for a few minutes to a few hours; CREATE during that window returns "Product ID already in use". Prefer bumping product_id in config (e.g. v1v2) for zero downtime.off

Per-region PPP pricing (1.6.0+)

Subscriptions and one-time products are fanned out to every Play-supported region (~175) with purchasing-power-parity-adjusted USD prices — not just the region you list explicitly. The multiplier table is Steam/Spotify-inspired (iosdevmax/ppp-pricing, MIT, bundled at src/data/ppp-tiers.upstream.json):

TierMultiplierSample regions
Very low0.30AR, EG, PK, BD, NG, KE
Low0.35IN, VN, PH, ID, UA, KZ
Lower-mid0.45BR, TR, TH, MY, CO, RO, BG
Mid0.60MX, ZA, CL, PL, HU, GR, PT, CZ
Upper-mid0.80KR, JP, TW, ES, IT, IL, SA, AE
Base1.00US, CA, GB, AU, NZ, FR, DE, NL, SG
High1.10CH, NO, DK, SE, FI, IS, LU

Prices are rounded to .99 endings. Regions outside the table fall back via a closest-neighbour lookup (e.g. PR → US, XK → AL); anything still missing uses 0.60 (mid tier).

Each region's price is in that region's native currency (AE → AED, JP → JPY, IN → INR, etc.) — Google Play rejects mismatched currencies (HTTP 400 "Invalid currency for region code AE: expected AED but got USD").

The CLI calls Google's pricing:convertRegionPrices endpoint with your USD base price once per (package, base price) tuple. That single call:

  1. Returns the authoritative billable region list (sanctioned countries like AF / IR / KP / SY are excluded — they don't appear in the response).
  2. Returns fair FX-converted prices in each region's native currency.

Then KAppMaker multiplies each native price by the region's PPP multiplier and charm-rounds:

  • Decimal currencies (USD, EUR, GBP, INR, BRL, etc.) — nearest .49/.99 ending, ties rounding up (1.13.18+). So INR 415 × 0.35 ≈ 145.25 → INR 145.49, and USD 6.99 × 0.35 ≈ 2.45 → USD 2.49 (pre-1.13.18 floor+.99 gave 2.99).
  • Zero-decimal currencies (JPY, KRW, CLP, ISK, VND, etc.) — round to the nearest X99 / X9 / integer. So JPY 750 × 0.80 = 600 → JPY 599.

Typical apps with 5 distinct base prices (weekly sub + yearly sub + 3 credit packs) make ~5 convertRegionPrices calls per setup run.

regionsVersion 2022/02 drift override (1.6.11+)

Google's monetization API requires regionsVersion.version and "2022/02" is the only documented value as of 2026. Several regions have drifted since that snapshot — Google's live API returns one currency but 2022/02 expects another. The CLI handles this transparently:

RegionLive API2022/02 expectsResolution
BG (Bulgaria)EURBGNConvert EUR → BGN via the 1 EUR = 1.95583 BGN peg
HR (Croatia)EUREUR ✓No override (Google updated 2022/02 retroactively in 2023)
CI (Côte d'Ivoire)XOFUSDReplace with USD anchor; PPP multiplier still applied
CM (Cameroon)XAFUSDSame
SN (Senegal)XOFUSDSame
MN (Mongolia)billableNOT BILLABLEDrop entirely (no currency works at 2022/02)

Net effect: typical products land with all ~173 billable regions AVAILABLE (only MN drops out). Google's storage layer auto-converts each region's submitted currency to the user-facing display currency at runtime — e.g. we send BG: BGN, Google shows EUR to Bulgarian users; we send CI: USD, Google shows XOF to Ivorian users.

"New countries and regions" set to AVAILABLE (1.6.11+)

Both subscriptions (otherRegionsConfig) and one-time products (newRegionsConfig) are now always set to availability: AVAILABLE with USD + EUR anchors when a USD anchor is present in your config. In Play Console this surfaces as "New countries and regions: Available" — any region Google adds to its billable catalog in the future automatically gets pricing derived from those anchors, no maintenance needed.

Stuck legacy products (NEVER_BILLABLE regions)

If Google's old catalog stored a NEVER_BILLABLE region (currently just MN) on an existing product, that PATCH can't be completed at regionsVersion=2022/02 — Google won't accept MN's currency but also refuses to drop the region. The CLI detects this deadlock, marks the product as "stuck" in its final summary, and offers three fix paths in order of safety:

  1. Recommended — bump the product_id in your config (e.g. credit_pack_10_499_myappcredit_pack_10_499_myapp_v2). Fresh product, full PPP fan-out, zero downtime.
  2. kappmaker gpc iap push --recreate-stuck — DELETEs the stuck product(s) and recreates them fresh. WARNING: Google soft-deletes for a few minutes to a few hours after deletion. CREATE during that window returns "Product ID already in use". Plan downtime if your app code references these IDs in production.
  3. Manually delete on Play Console UI, wait for the reservation window to clear, then re-run.

Simpler alternative: ppp_enabled: false

If you don't want PPP discounting and would prefer a uniform USD-anchor price across all regions (Google handles the FX conversion at display time), set "ppp_enabled": false on the base plan or one-time product:

{
"base_plan_id": "autorenew-yearly-2999-v1",
"billing_period": "P1Y",
"ppp_enabled": false,
"regional_configs": [
{ "region_code": "US", "price": "29.99", "currency_code": "USD" }
]
}

The CLI then submits only your US entry in regionalConfigs plus otherRegionsConfig: { usdPrice, eurPrice, newSubscriberAvailability } (for subscriptions) or newRegionsConfig: { availability, usdPrice, eurPrice } (for one-time products). Google fans out the USD anchor to every billable region using its pricing-template FX algorithm — every region gets a price, just without PPP discounts in lower-income markets. Payload is ~1 entry instead of ~150.

The trade-off: simpler config but you lose the IN ≈ 0.35×, AR ≈ 0.30× discounting that increases conversion in those markets.

Override rule: any region you list explicitly in regional_configs (subscription) or prices (IAP) wins. PPP fills only the regions you didn't list:

{
"sku": "credit_pack_10_499_myapp",
"default_price": { "region_code": "US", "price": "4.99", "currency_code": "USD" },
"prices": [
{ "region_code": "DE", "price": "5.49", "currency_code": "EUR" }
]
}

US gets your default_price, DE keeps your explicit 5.49 EUR, the other ~173 regions get PPP-adjusted USD prices derived from the US anchor.

Opt-out: set "ppp_enabled": false on a subscription base plan or IAP to skip the fan-out and stay restricted to your listed regions.

Why explicit fan-out: Google's otherRegionsConfig / newRegionsConfig auto-conversion (used in 1.5.x) fanned out unreliably — many users saw products listed in only 1-2 regions even with valid USD/EUR anchors. Per-region fan-out is now the default and works.

Default credit packs

The Assets/googleplay-config.json template ships with three default consumable IAPs (credit packs) that match the App Store Connect and Adapty templates:

PackCreditsPriceSKU
Basic Credit Pack10$4.99credit_pack_10_499_{appname}
Pro Credit Pack30$9.99credit_pack_30_999_{appname}
Ultimate Credit Pack80$19.99credit_pack_80_1999_{appname}

Format: credit_pack_{credits}_{priceDigits}_{appname}. The same product ID is used across ASC, Play, and Adapty so app code only needs one constant.

Auto-fill: triggers on any in_app_products[] entry with a credits numeric field. Custom IAPs without a credits field keep whatever sku you write.


gpc monetization push

Re-run only the subscription + IAP push steps from gpc setup — useful for refreshing PPP pricing across all ~175 regions without repeating listings, data safety, or the manual-checklist prompt.

# Push everything (subscriptions + IAPs)
kappmaker gpc monetization push

# Subscriptions only
kappmaker gpc monetization push --subscriptions-only

# IAPs only
kappmaker gpc monetization push --iap-only

# Custom config path
kappmaker gpc monetization push --config ./path/to/googleplay-config.json

# Re-create products stuck due to regionsVersion 2022/02 drift
kappmaker gpc monetization push --recreate-stuck

Validates the service account and probes app state (requires at least one uploaded build — internal testing track is enough). Calls the same setupSubscriptions / setupInAppProducts functions as gpc setup — fully idempotent: existing products are PATCHed with refreshed PPP regional fan-out.

FlagDescriptionDefault
--config <path>Path to JSON config file./Assets/googleplay-config.json
--subscriptions-onlyPush subscriptions only, skip IAPs
--iap-onlyPush IAPs only, skip subscriptions
--recreate-stuckDELETE + recreate products stuck due to regionsVersion=2022/02 incompatibility

:::tip When to use this vs gpc setup Use gpc monetization push when you've already run the full setup and just want to sync pricing (e.g. after upgrading KAppMaker for a PPP fix, or after adding a new product to the config). Use gpc setup for initial setup or when you also need to update listings or data safety. :::


gpc data-safety push

Push only the data safety declaration. Faster than running the full setup when iterating on the privacy answers.

kappmaker gpc data-safety push

Two Ways to Configure

1. Structured JSON (recommended)data_safety.answers overlay on top of defaults:

"data_safety": {
"apply_defaults": true,
"answers": {}
}

Default data types collected:

Data typePlay Question / ResponseCollection purposes
User IDsPSL_DATA_TYPES_PERSONAL/PSL_USER_ACCOUNTApp functionality, Account management
Device IDPSL_DATA_TYPES_IDENTIFIERS/PSL_DEVICE_IDApp functionality, Advertising or marketing, Account management
Crash logsPSL_DATA_TYPES_APP_PERFORMANCE/PSL_CRASH_LOGSAnalytics
DiagnosticsPSL_DATA_TYPES_APP_PERFORMANCE/PSL_PERFORMANCE_DIAGNOSTICSAnalytics
Other performancePSL_DATA_TYPES_APP_PERFORMANCE/PSL_OTHER_PERFORMANCEAnalytics
App interactionsPSL_DATA_TYPES_APP_ACTIVITY/PSL_USER_INTERACTIONAnalytics

Default data handling: collected only (not shared), processed ephemerally, collection is required, encrypted in transit.

To override a specific answer, add keys to answers:

"data_safety": {
"apply_defaults": true,
"answers": {
"PSL_DATA_TYPES_LOCATION/PSL_APPROX_LOCATION": true
}
}

2. Escape hatch: CSV file — upload a Play Console-exported CSV verbatim:

"data_safety_csv_path": "Assets/data-safety.csv"

gpc app-check

Quick read-only probe to verify that an app exists on Play Console. Useful for CI scripts.

kappmaker gpc app-check --package com.example.myapp

Exits 0 if found, 2 if missing (prints the Play Console deep link).

Options

FlagDescriptionDefault
--package <name>Override the package name from config