storebound/skills/store-iap-setup/SKILL.md
CRT b63f2fdfbc Store operations: TestFlight wait / expire, IAP review submissions, listing pulls, release notes per language, offers, custom product pages, in-app events, reports, triage, keyword ranks, AdAttributionKit scan
- apple testflight wait --build N (progress, timeout, exit 0/1/7) and expire --keep N | --older-than D (dry run, asks)
- apple iap-submit: IAP / subscription / group versions into a review submission; first of a type only with the app
  version (--with-version), --no-submit keeps the draft; read-only MCP plan
- apple listing pull, play listing pull (diff, --write), catalog pull (config proposal, never rewritten)
- whats-new --languages / check / apply: one file per language, agent or hook translation, per-store limits
- apple offer-codes, win-back, promoted, preorder; Play pre-registration as a guide (no API)
- apple cpp and apple events: list, create, upload, submit (asks)
- reports: Apple sales / finance, Play earnings / sales into .storebound/reports with summary and CSV
- triage: TestFlight crash / feedback and Play error reports grouped by signature, device, OS
- aso rank: iTunes Search API ranks and competitors, rate limited
- ads scan: AdAttributionKit / SKAdNetwork Info.plist and source checks
- tests against recorded, sanitized answers; skills, README, CHANGELOG
2026-10-11 23:58:17 +08:00

10 KiB

name description
store-iap-setup Create and price in-app purchases (consumables, non-consumables like "remove ads" or a Pro unlock, non-renewing passes, coin/gem packs, starter packs) on Google Play and App Store Connect from one product list - regional prices, local overrides, a price sheet CSV for many products and regions (diff first, future-dated App Store changes), activation, review screenshots, submitting IAPs and subscriptions to review through the API (first ones together with an app version), offer codes for one-time purchases, license/sandbox testers. Use when the user sets up, prices, re-prices or tests in-app products/IAPs, checks that both stores match (catalog diff), or hits Play onetimeproducts / inappproducts 403 or App Store IAP submission errors.

In-app purchases on both stores

CLI: storebound (or bunx storebound in a project that has it, or bun <toolchain>/cli/index.ts). One list in storebound.config.ts products[] drives both stores: id (same on both), type (consumable | non_consumable | non_renewing), usd base price, optional overrides ({ region: 'TR', currency: 'TRY', price: 99.99 }), listings per language (title <= 30, description <= 45 for Apple), reviewScreenshot, optional platforms: ['ios'] / ['android'] to sell it on one store only. Auto-renewable subscriptions are a separate list (subscriptions, store-subscriptions skill).

  • non_renewing: Apple NON_RENEWING_SUBSCRIPTION (access for a fixed time; your app tracks the expiry and offers restore through your account system). Google Play has no such type: storebound makes it a one-time product your app consumes when the access ends (or model it as a prepaid base plan in Play Console).
  • Cross-store parity: storebound catalog diff (MCP catalog_diff, read-only) compares the config with both stores: missing / extra products, type, US and override prices, localizations, and regions where the two stores price the same product more than 10 % apart (--threshold).
  • Offer codes for one-time products exist on the App Store API (inAppPurchaseOfferCodes); storebound does not create them yet (App Store Connect > the IAP > Offer Codes). Play promo codes have no API (Play Console > Monetize > Promo codes).

Prerequisites

  • Play: a build with com.android.vending.BILLING uploaded to any track; payments profile set up.
  • Apple: Paid Applications agreement + banking + tax active (Business > Agreements), the app record exists.

Google Play (API)

  1. storebound check, then storebound play products --dry (read-only: per product create, unchanged, or exactly what would change: listings, regional prices old -> new, dropped regions, activation). Show it to the owner.
  2. storebound play products. Per product: pricing:convertRegionPrices (USD -> every region at local price points, and the regionsVersion), your overrides on top, PATCH .../onetimeproducts/{id}?allowMissing=true&regionsVersion.version=... (create or update), then oneTimeProducts/{id}/purchaseOptions:batchUpdateStates to activate (new options are DRAFT). New products and listing text go straight through. Price, region or activation changes of an EXISTING product are the owner's decision: the run shows the diff and asks in a terminal; without a terminal (MCP, CI, the desktop app's normal write) they are skipped with a warning, the listing text still updates. The owner confirms in a terminal, or applies prices with the desktop Prices "Apply" (owner action, prices import).
  3. Verify: storebound play status lists every product with buy:ACTIVE.
  4. Test: Play Console > Settings > License testing: add tester Gmail accounts; install from the internal testing link; purchases are free test orders.

App Store (API + UI)

  1. storebound apple iap --dry, then storebound apple iap: creates missing products, localizations, a price schedule (USA base + overrides; Apple equalizes the rest) and availability. Existing schedules and availability are never changed by it; price changes go through storebound prices import (diff first, then the owner confirms).
  2. Review screenshot per product (a real device size, e.g. 1290 x 2796, showing the item in the shop): storebound apple iap-review. States move to READY_TO_SUBMIT when complete.
  3. Send them to App Review (the owner's step): storebound apple iap-submit --dry shows the plan: which version of each product goes in, what is left out and why, and which products are the first of their type for this app.
    • Apple's rule (App Store Connect Help, "Submit an In-App Purchase"): the first consumable, non-consumable, non-renewing subscription and auto-renewable subscription of an app must be submitted with a new app version; later ones of an approved type go alone as long as the app has at least one approved version. A new subscription goes with its subscription group while the group is not approved. Everything reviewed together sits in one draft submission, up to 200 items. Content-hosted IAPs can no longer be submitted.
    • Through the API (App Store Connect API 4.4.1+): each product has versions (inAppPurchaseVersions, subscriptionVersions, subscriptionGroupVersions) that go into a review submission as items, next to the app version when needed. So the first ones no longer need the web UI: --with-version puts the editable app version (storebound apple listing --version x.y.z --build N first) into the same submission, and then submitting it submits the app too.
    • The real run asks before adding the items and again before submitting; --no-submit stops at the filled draft so the owner presses "Submit for Review" in App Store Connect. --new-version makes a new draft version of an approved or rejected product first (changed name, description or screenshot). Never an MCP tool; MCP has the read-only plan apple_iap_review_plan. --json prints the plan for scripts.
  4. Offer codes (free or discounted) for one-time purchases: see the app-store-promotion skill (storebound apple offer-codes).
  5. Test: Sandbox testers (Users and Access > Sandbox) on device; or a StoreKit configuration file in Xcode for local runs (those transactions are signed locally and fail server-side receipt checks).

Many prices at once: the price sheet (both stores)

For price changes after the products exist (sales, regional pricing, a new price everywhere), keep one CSV: product,store,region,currency,price,start_date (store = apple | play | both; region ISO alpha-2; start_date App Store only, a future YYYY-MM-DD).

  1. storebound prices export writes the current prices of both stores to store/prices.csv (--with-automatic adds Apple's equalized prices as kind=auto, for reference; they are ignored on import).
  2. Edit the rows. Apple only takes its own price points: an exact value per territory (the error lists the neighbours); Play takes any price in the region's currency.
  3. storebound prices import --dry (MCP: prices_diff) prints the diff table: product, store, region, from, current, new, change %. Show it to the owner.
  4. The owner runs storebound prices import (asks first; not an MCP tool). Apple: one new price schedule per changed product (base territory, default the current one or prices.baseTerritory, plus every manual price; territories without one follow Apple's equalization). Play: the purchase option's regional prices are replaced; regions the product has no price for get convertRegionPrices from the USD base, --reconvert resets every region the CSV does not list.
  • Future-dated changes (start_date) are App Store only; Play has no scheduled prices (change them on the day). The same CSV feeds subscription prices: storebound apple subscriptions --csv store/prices.csv (dated rows become scheduled subscription price changes) and storebound play subscriptions --csv store/prices.csv.
  • Apple's IAP pricing guide warns that a price increase cannot be reverted once it is in effect: tell the owner.
  • Prices are public the moment the stores apply them. Never import without the owner reading the diff.

Gotchas

  • Play legacy inappproducts returns 403 "Please migrate to the new publishing API" -> use monetization.onetimeproducts.
  • Path casing: PATCH works on onetimeproducts, but list + purchaseOptions:batchUpdateStates need oneTimeProducts; the wrong casing answers with an HTML 404 page.
  • Play has no consumable flag: the app consumes the purchase after granting it. Use legacyCompatible: true so older billing libraries see the product by id.
  • Apple: first IAPs must ship with an app version ("FIRST_*_MUST_BE_SUBMITTED_ON_VERSION" from the old inAppPurchaseSubmissions). If the version is already in review without them: cancel the review submission (storebound apple status --cancel-submission <id>, ask the owner), then storebound apple iap-submit --with-version (or "Add for Review" in the UI) and resubmit.
  • Not verified against a live account yet: adding the first IAP of a type next to the app version through reviewSubmissionItems follows the API 4.4.1 release notes and the 4.5.1 spec (the item takes appStoreVersion and inAppPurchaseVersion in one submission). If App Store Connect refuses it, --no-submit leaves the draft and the UI "Add for Review" finishes the job. The deprecated inAppPurchaseSubmissions / subscriptionSubmissions are not used.
  • Apple needs an exact existing price point for each manual price; the error lists the nearest ones.
  • Apple IAP display name 2-30 characters, description max 45 (App Store Connect Help > Reference > "In-app purchase information"; an older App Store marketing page says 55, but App Store Connect enforces 45).
  • Product ids can never be reused after deletion on either store. Never delete without the owner.
  • Do not put prices or product names in store descriptions; say "offers optional in-app purchases".
  • If the app ships without real billing yet, hide every purchase surface in store builds (review rejects mock purchases) and answer "digital purchases: No" in IARC.