storebound/skills/store-iap-setup/SKILL.md

11 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 (with their first prices) and listing text go straight through; --yes confirms activating the purchase option of an existing product. Price or region changes of an EXISTING product are the owner's decision and --yes never confirms them (also not the --yes that flow run --apply --yes adds): the dry run marks those rows owner and lists them in a "price changes on existing products (held)" section, and a real run writes everything else, prints price change on <id> (<from> -> <to>) needs the owner: re-run with --allow-price-changes and exits 3 (declined; a flow stops there). A terminal "y" never covers them either: only --allow-price-changes does, and then a second question lists each old -> new. So adding new products to the config and re-running is safe without --only. After the owner's yes in chat for exactly that diff, run storebound play products --yes --allow-price-changes, or the owner uses the desktop Prices "Apply" (owner action).
  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's yes and --allow-price-changes).
  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). After the owner canceled a submission the versions are DEVELOPER_REJECTED: they go into the next submission as they are (the plan says "add (developer rejected, re-submitted as is)"); no new version is needed. 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. After the owner's yes for that diff: storebound prices import --yes --allow-price-changes (not an MCP tool; --yes alone only sets first prices of products that have none and exits 3 naming every held price change). 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 and resubmit.
  • Verified on a real first release (2026-10-12): storebound apple iap-submit --with-version --yes (after the owner's yes: it submits) added the first IAPs to the existing draft submission that already held version 1.0.0 and submitted them, first try, through reviewSubmissionItems. Do not use the console for this: the IAP page's "Add for Review" menu did not react to scripted clicks, and the version page no longer has an "In-App Purchases and Subscriptions" section (pitfall 28). 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.