--- name: app-store-promotion description: App Store promotion tools for a mobile app or game through the App Store Connect API - offer codes for one-time purchases, win-back offers for lapsed subscribers, promoted in-app purchases on the product page, pre-orders per territory, custom product pages (screenshots per audience, deep links) and in-app events, with Apple's limits; Google Play pre-registration as a console guide (no API). Use when the user wants promo codes or discounts for IAPs, to win back subscribers, to feature purchases on the product page, a pre-order or pre-registration launch, ad-specific product pages, or an in-app event. --- # App Store promotion (offers, promoted IAPs, pre-orders, custom product pages, in-app events) Everything here is App Store Connect API (`cli/lib/offers.ts`, `cli/lib/cpp-events.ts` cite the endpoints and Apple's Help pages). Each command has `--dry` and `--json`. What customers can see or redeem is the **owner's** step: those commands show the dry run, then ask in the terminal, and none of them is an MCP tool. MCP has the read-only and nothing-public parts (`apple_cpp_list | create | upload`, `apple_events_list | create | upload`, `apple_preorder_status`). ## Offer codes for one-time purchases Config, on the product (consumable, non-consumable, non-renewing): ```ts products: [{ id: 'com.example.app.pro', type: 'non_consumable', usd: 9.99, listings: { ... }, offerCodes: [{ name: 'Launch week', eligibility: ['NON_SPENDER'], usd: 4.99, // or free: true customCodes: [{ code: 'LAUNCH26', count: 1000, expires: '2026-12-31' }], oneTimeCodes: [{ count: 500, expires: '2027-01-31' }] }] }], ``` - `storebound apple offer-codes --dry`, then without `--dry`: creates missing campaigns (by name) with prices for every territory (the USA price point of `usd` and its equalizations, or `territories: ['USA', 'DEU']`). A campaign alone lets nobody redeem anything. - Codes only with `--codes` (asks): custom codes (anyone who has the code, up to 25,000 redemptions, letters and digits up to 64) and one-time-use batches (500-25,000, expiry at most 6 months). One-time codes are saved to `.storebound/offer-codes/` (gitignored): treat them like vouchers, never paste them into chat, issues or commits. - Apple's limits (Help, "Create offer codes for In-App Purchases"): 10 active offers per app, 1 million codes per app per quarter; eligibility NON_SPENDER (never bought in the app), ACTIVE_SPENDER (bought in the last 30 days), CHURNED_SPENDER (bought more than 30 days ago). Needs iOS 16.3+. Subscription offer codes are `offerCodes` on a subscription (`storebound apple subscriptions --codes`, store-subscriptions skill). - Play promo codes have no API: Play Console > Monetize > Promo codes. ## Win-back offers (subscriptions) ```ts winBack: [{ id: 'come-back-3m', type: 'pay-up-front', duration: 'P3M', usd: 9.99, paidMonths: 6, lapsedMonths: { min: 2, max: 12 }, waitMonths: 6, start: '2026-11-01', end: '2026-11-30', priority: 'high', promote: false }], ``` `storebound apple win-back --dry`, then the owner runs it (asks). Rules (Help, "Set up win-back offers"): the subscription must be approved; paid at least 1-24 months or 3, 4, 5 years; lapsed between min and max months (1-24, both required); wait between offers 2-24 months; start at the earliest tomorrow and run at least 3 days; at most 5 running per storefront and 350 per subscription. Reference name, offer id and price cannot change later; the offer id must not be reused in the group. An offer goes live by itself on its start date (Manage Subscriptions, the in-app offer sheet on iOS 18+, the App Store with `promote: true` and an approved subscription image). ## Promoted in-app purchases `apple: { promoted: { products: ['com.example.app.pro', 'com.example.app.coins_100'] } }` (order = display order, at most 20). `storebound apple promoted --dry` shows the status and the plan: missing ones are created **disabled**, the order is set. `--enable` switches them on (asks; they show on the product page once approved). Each needs its promotional image in App Store Connect, and the app must handle purchases that start on the App Store (StoreKit `PurchaseIntent`; StoreKit 1 `paymentQueue(_:shouldAddStorePayment:for:)`). Without `apple.promoted` the command only prints the status. ## Pre-orders - `storebound apple preorder status` (MCP: `apple_preorder_status`): per territory available, release date, pre-order on, publish date, content status. - `storebound apple preorder enable --release-date 2026-12-01 [--territories USA,DEU] --dry`, then for real (asks). Apple: a version must be approved first; the release date is 2-180 days ahead for a new app (up to 365 days for an existing app in a new region); a territory where the app is already released cannot go on pre-order. - `storebound apple preorder end [--territories USA]` ends the pre-order and **releases the app there now** (asks). - **Google Play pre-registration has no API** (the androidpublisher v3 reference lists no such resource): Play Console > Grow users > Pre-registration (set it up on a testing / production track that is not yet available, optional pre-registration reward as a one-time product). Pre-registration ads: Google Ads. ## Custom product pages ```ts apple: { customProductPages: [{ name: 'Students', variant: 'students', deepLink: 'https://example.com/open/students', promotionalText: { en: 'Class notes that sync.', de: '...' } }] }, ``` 1. Screenshots: `storebound shots render --variant students` (a `shots.variants` entry), store-screenshots skill. 2. `storebound apple cpp create --dry`, then for real (MCP `apple_cpp_create`): the page, its first version and a localization per App Store locale. 3. `storebound apple cpp upload --name Students --dry` (MCP `apple_cpp_upload`): the variant's renders per locale, MD5-idempotent. 4. Owner: `storebound apple cpp submit --name Students` (asks; App Review, independent of an app update), after approval `storebound apple cpp visible --name Students` (asks). `apple cpp list` shows each page's URL for ads. Limits: 70 pages per app; promotional text 170; the deep link works from iOS / iPadOS 18 once approved. Screenshot sets are deprecated in API 4.5.1 in favour of the App Asset Library; they still work and storebound uses them. ## In-app events ```ts apple: { events: [{ referenceName: 'Winter Challenge', badge: 'CHALLENGE', purpose: 'KEEP_ACTIVE_USERS_INFORMED', schedule: { publishStart: '2026-12-01T00:00:00Z', eventStart: '2026-12-10T00:00:00Z', eventEnd: '2026-12-20T00:00:00Z' }, listings: { en: { name: 'Winter Challenge', shortDescription: 'Finish ten lists before New Year', longDescription: '...' } }, media: { card: 'store/events/winter/{lang}-card.png', details: 'store/events/winter/{lang}-details.png' } }] }, ``` `storebound apple events create --dry` / `upload --name ...` (MCP `apple_events_create` / `apple_events_upload`), then the owner runs `storebound apple events submit --name ...` (asks; once approved the event publishes by its schedule). Limits (Help, "Offer In-App Events"): reference name 64, event name 30, short description 50, long description 120; 15 minutes to 31 days; published at most 14 days before it starts; 10 published and 15 approved events per app. Media: event card 16:9 (1920x1080 to 3840x2160), details page 9:16 (1080x1920 to 2160x3840), PNG or JPEG; storebound checks the size before uploading. Event text is held to the same honesty rules as the listing: a real event in the app. ## Gotchas - Free offer prices (offer codes, win-back) are sent as territories without a price point, as the API schema allows; not yet verified against a live account. A refusal names the field: create that one offer in App Store Connect. - Everything with a start date goes live without another click: read the dates with the owner. - No manipulative offers: no fake scarcity in event text, no misleading "free" when a purchase is needed (`purchaseRequirement` says it).