storebound/skills/app-store-promotion/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

100 lines
7.9 KiB
Markdown

---
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).