storebound/skills/store-screenshots/SKILL.md

30 KiB

name description
store-screenshots Produce store screenshots and the Play feature graphic for a mobile app or game in every required size and many languages - capture raw frames from the running app or game (web build via playwright, iOS Simulator via simctl, Android emulator or device via adb, or a folder of frames from any engine), write per-language marketing copy (incl. RTL Arabic and CJK fonts), render captioned scenes with device frames, review them in a local gallery, A/B variants for Play store listing experiments and App Store Product Page Optimization tests (created and filled through the API), JPEG copies for Play, and upload to both stores. Use when the user needs App Store / Google Play screenshots, preview images, a feature graphic, localized screenshot captions, or screenshot sizes.

Store screenshots

CLI: storebound (or bunx storebound in a project that has it, or bun <toolchain>/cli/index.ts). Needs ffmpeg, and playwright in the project (bun add -d playwright; it uses Playwright's Chromium or an installed Google Chrome) for rendering and web capture. iOS Simulator capture needs macOS + Xcode (xcrun); Android capture needs adb (platform-tools).

An app that is already live: pull its art first (the onboarding default)

Do not render art the stores already have. For an existing app (storebound onboard does this step too):

  1. storebound art pull --dry: per store, language and set what would be added, what is already the same, and which local files differ from the store (your own renders: kept, listed as conflicts).
  2. storebound art pull --write: downloads the originals (App Store screenshot sets and app previews per locale and display type, the App Store icon; per Play language the icon, feature graphic and phone / 7" / 10" / TV / Wear screenshots) into store/renders/<lang>/<size>/NN.png, feature/1024x500.*, <icons.out>/app-store-1024.png and play.icon, plus store/renders/pulled.json (sha256, store, URL host, store checksum; no URLs, no personal data). Read-only on the stores. A re-run changes nothing; --force (the owner's call) replaces differing local files.
  3. Then check / validate treat the files like rendered art: render sources (icon artwork, logo, captions) become notes, pulled files pass the local checks as the store's own, and sets the store has none of are not demanded locally. play images --dry / apple screenshots --dry show "unchanged": a pulled file counts with the store's own checksum until you edit or re-render it.
  4. Change one image: edit or replace the file (desktop: Store art > open the image > Replace with a file), then the upload's dry run shows exactly that set. Render new art (the steps below) only when the owner wants new screenshots. Old display types without a size key here (e.g. APP_IPHONE_55) land in apple-<type>/ and are kept, never checked or uploaded as another size. store/renders can be large (hundreds of MB) and never goes into git: art pull --write, init and onboard add it (/store/renders/, relative to the repo root) to the repo's .gitignore when it is not ignored yet.

Sizes (what each store wants)

size key pixels where
ios69 1320 x 2868 App Store 6.9" (APP_IPHONE_67, "Dynamic Island (large display)"); the set storebound requires
ios65 1284 x 2778 App Store 6.5" (APP_IPHONE_65)
iphone-duo-outer 1398 x 2034 (or 2034 x 1398) App Store "iPhone Duo" outer display; optional, only with apple.iphoneDuo: true (tablet frames)
iphone-duo-inner 2007 x 2853 (or 2853 x 2007) App Store "iPhone Duo" inner display; same switch
ipad13 2064 x 2752 App Store 13" iPad (APP_IPAD_PRO_3GEN_129), required when the app runs on iPad; only with apple.ipad: true
mac 2880 x 1800 (also 2560 x 1600, 1440 x 900, 1280 x 800) Mac App Store (APP_DESKTOP), 16:10 landscape only; only with apple.mac: true, uploaded to the macOS version
play 1080 x 1920 Play phone (2-8, ratio at most 2:1)
tab7 1080 x 1920 Play 7" tablet (tablet frames, Android tablet body); optional, see play.tablets
tab10 1440 x 2560 Play 10" tablet; optional, see play.tablets
chromebook 1920 x 1080 Play Chromebook screenshots (landscape layout); only with play.chromebook: true; console only (no API image type)
feature 1024 x 500 Play feature graphic (required)

Sources (re-check before a release; Apple renames display classes often):

  • Apple "Screenshot specifications": https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications (13" iPad: 2064 x 2752 or 2048 x 2732, landscape too, "Required if app runs on iPad"; 1-10 per size; JPEG / PNG, no alpha or transparency). As of October 2026 the page marks "iPhone with Dynamic Island (medium display)" (1206 x 2622 / 1179 x 2556) as the required iPhone class, and says a class without its own screenshots gets scaled ones from the next class: medium from Face ID large (6.5"), 6.5" from Dynamic Island large (6.9"). So the 6.9" (+ 6.5") set still covers every iPhone, and storebound keeps 6.9" as the default. For pixel-exact art on 6.3" phones (or if App Store Connect ever refuses to submit without the medium set), set apple.iphoneMedium: true: the iphone-medium size key (1206 x 2622, uploaded as APP_IPHONE_61, a mapping inferred from the enum, not verified) is then rendered, checked and uploaded like the others. It stays off by default.
  • ASC API ScreenshotDisplayType: https://developer.apple.com/documentation/appstoreconnectapi/screenshotdisplaytype (there is no "13 inch" value: the 13" iPad set is APP_IPAD_PRO_3GEN_129).
  • Play graphics: https://support.google.com/googleplay/android-developer/answer/9866151 (screenshots 320-3840 px, at most 2:1, JPEG or 24-bit PNG, 8 MB; feature graphic 1024 x 500).

iPad. Set apple.ipad: true only if the app runs on iPad (iPhone-only apps skip the iPad set). Then the default sizes include ipad13 (3:4 layout), apple screenshots uploads it, validate requires it, and tablet frames default to the 13" iPad screen (1032 x 1352 CSS px @2x below a 24 px bar), so an iPad Pro 13-inch / iPad Air 13-inch simulator screenshot fits after the bar is cropped. Play tablet sizes (tab7 / tab10) reuse the same tablet frames, drawn in a plain Android tablet body instead of the iPad one.

iPhone Duo (foldable). App Store Connect's "App Previews and Screenshots" tab now lists three classes: "iPhone with Dynamic Island (medium display)" (required for iPhone; a 6.9" or 6.5" set scales down into it, so validate passes with the ios69 set), "iPhone Duo" and "iPad 13" display" (required with apple.ipad). Duo takes an outer (1398 x 2034) and an inner (2007 x 2853) set, portrait or landscape, PNG / JPEG without alpha, 1-10 each. It is optional now and reportedly required from April 2027 for builds with the iOS 27.1 SDK or later; check / validate print an info note until then (a warning after, never a blocker). apple.iphoneDuo: true adds both sizes to shots render / shots check / the gallery. The API (4.5.1) has no Duo ScreenshotDisplayType yet, so apple screenshots skips the Duo folders with a message and the deep link to the version page; add them in the console's iPhone Duo tab until then (one constant, IPHONE_DUO_DISPLAY in cli/shots/spec.ts, takes the names once the enum has them). No Duo app preview sizes are published. Google Play has no foldable slot: foldables show the 7" / 10" tablet sets below.

Mac App Store. Apple's Screenshot specifications (checked 2026-10-20) list four 16:10 Mac sizes: 1280 x 800, 1440 x 900, 2560 x 1600 and 2880 x 1800, PNG / JPEG without alpha, 1-10. apple.mac: true adds the mac size key (2880 x 1800): shots render lays it out in landscape, shots check accepts exactly those four sizes (no portrait, no alpha) and requires the set, and apple screenshots --platform macos uploads it as APP_DESKTOP to the macOS version (versions are per platform, pitfall 79; the iOS run uploads the iOS sets only and says where the Mac set goes). validate prints an info note when the app has a macOS version but no Mac screenshots. Frames: landscape captures in <frames>/<lang>/landscape/<name>.png, shots.landscape.size CSS px (default 1440 x 900 @2x).

Landscape layout (Mac, Chromebook, Play tablets). Copy in a column on one side (shots.landscape.side, default left), the scene's first frame on the other. shots.landscape.frame: 'window' draws a Mac window (title bar with the three buttons, the default for mac), 'none' the bare frame with rounded corners (the default for Play sizes). Both templates (game and app) have it. Play: play.chromebook: true renders the chromebook size (1920 x 1080; the Play API has no Chromebook image type, so play images prints the Main store listing deep link instead of uploading), and shots.landscape.playTablets: true renders tab7 / tab10 in landscape (1920 x 1080 / 2560 x 1440), which Play's tablet slots accept. Badges are not drawn in landscape.

Safe margins (every template, every size, both stores). Text and logo keep about 7 % of the width left and right and 5 % of the height at the top; the logo scales to fit inside. Each render records the layout boxes of the logo, eyebrow, title lines and subtitle (and the feature graphic's logo or name) in report.json, and shots check fails any box closer than 6 % of the width to the left / right edge or 4 % of the height to the top (code margin). The measurement comes from the page's own layout, not from a pixel scan. Renders from older templates fail it: render again.

**Play tablets are optional.**Play tablets are optional. Play requires the 512 x 512 icon, the 1024 x 500 feature graphic and at least 2 screenshots (answer 9866151 above); 7" / 10" tablet screenshots are optional, but without them the app is not eligible for tablet featuring. play.tablets decides: unset = true when tablet renders (tab7/ or tab10/) already exist, else false; false = shots render, shots check, shots gallery, validate and play images never ask for them; true = they are rendered, and missing ones are errors. A phone-only project needs no setting: play images uploads the icon, feature graphic and phone screenshots and prints tablet screenshots: none (optional; ...).

Steps

  1. Screenshot switches. Give the app or game params for screenshots: language, demo data or a fixed seed, no ads / analytics / purchase UI, skip onboarding or the tutorial, jump to a screen or level, freeze at a moment. Frames must show the real app in use (store rule; for a game: real gameplay). Apps: sign in to a demo account with realistic, non-personal sample data; never show real user data.
  2. Capture. Configure shots.capture with a source (or pass --source), then storebound shots capture --lang en --dry to see the plan, and without --dry to capture. Frames land in <shots.frames>/<lang>/<phone|tablet>/<shot>.png, local, not committed.
    • web (default): baseUrl, query with {lang}, shots: path / waitFor / eval / delayMs / device. Phone 440 x 902 @3x and tablet 800 x 1240 @2x by default; the template draws the status bar above. Speed (many languages): frames run in parallel browser contexts (--concurrency N, capture.concurrency; default min(3, cores / 4) since game boots are GPU-heavy). capture.session: 'per-language' boots the app once per language and device and switches shots in that page (per shot navigate: 'spa' = pushState + popstate to its path, then waitFor / eval; 'reload' = goto in the same page); a shot whose waitFor fails there within reuseTimeoutMs (15000) gets a fresh page. Re-runs skip frames whose inputs did not change (shot entry, URL, frame size, the language's copy, the build: capture.build files / folders, else the HTML and scripts at baseUrl, fonts) via .storebound/capture-cache.json; --only-missing captures only missing files, --force everything. Each progress line shows n/total, ETA and the time per step (goto, wait, eval, delay, shot); the end lists the slowest frames and steps, and .storebound/capture-report.json keeps them. Put the slow step right (a long waitFor = the game reaching the scene: add a URL switch that jumps there). Screens behind a login: capture.auth (see Signed-in apps).
    • ios-sim (Unity, Godot, Flutter, native, Capacitor: any Simulator build): ios.device (simulator name or UDID, e.g. a 6.9" iPhone), ios.bundleId, optional ios.app (.app to install), ios.args. Per shot: args (launch arguments, read them with UserDefaults / your engine) and / or url (deep link). Language goes in as -AppleLanguages / -AppleLocale; status bar is forced to 9:41, full battery. After a cold boot the app is launched once and given ios.warmupMs (default 8000) before the first shot; without it the first frame of a fresh simulator is often blank. A simulator storebound boots is shut down afterwards (--keep-booted keeps it). For iPad shots set ios.devices.tablet (e.g. "iPad Pro 13-inch (M5)") and device: 'tablet' | 'both' per shot.
    • android-emu: android.package, optional serial, activity, apk. Per shot: url (VIEW intent) and / or typed extras. Demo-mode status bar; per-app language on Android 13+ (older: pass {lang} in an extra). Nothing is wiped or rebooted.
    • folder: frames made any other way; shots capture (and shots check) only verify that every frame the scenes use exists with the right aspect ratio and resolution. {lang}, {locale}, {shot} are replaced in args, extras and URLs. Simulator / emulator screenshots lose cropTop rows (the status bar) so they fit the template: by default the frame's bar share per device class (162 px on a 6.9" iPhone, 48 px on a 13" iPad with apple.ipad); set cropTop: 120 or { phone, tablet } for other devices, and shots.phone / shots.tablet to that device's screen in points. Capture warns when a frame's aspect does not match. Give the app time to load: delayMs (simulator / emulator; 2500 default is short for a cold start) or waitFor (web).
  3. Copy. store/shots/copy.json: per language, per scene: eyebrow, title [white line, gold line], sub, badges big, small (game style). Short and true (only claim what is in the app); about 15 Latin characters per title line. Natural marketing tone per language, not literal translation; use the app's own terms.
  4. Fonts. storebound shots fonts downloads title/body fonts and a Noto subset for every non-Latin script found in the copy (CJK, Arabic, Hebrew, Devanagari, Thai, Cyrillic/Greek), then checks every character of every language (copy.json + config languages) against the cmap of the fonts that draw it. A character the title / body font lacks gets an automatic Noto fallback (WARN tr title: ş ğ not in Lilita One -> Noto Sans Display); no fallback at all fails with the language and the characters. Renders that used an automatic fallback (or a system font) are ERRORS in shots check: fix it with a title / body font that covers the language (Paytone One, Baloo 2, Rowdies for game titles; Montserrat, Inter, Nunito for body), or, if the owner accepts mixed glyphs, shots.fonts.fallback: ['Noto Sans Display']. Lilita One lacks most of Latin Extended (Turkish ş ğ İ, Polish, Czech, Romanian) and Vietnamese. Re-run after copy or font edits (render refuses fonts that no longer cover the copy). A game's own brand font: put the file path (relative to the config; TTF / OTF / WOFF / WOFF2) or { family, file, license } into shots.fonts.title / body / fallback. The same coverage check runs on it, listed fallbacks are tried before Noto (pl title: ę ł ą Ł not in ER Display -> Noto Sans Display), the file is served in place (never copied) and its hash is in fonts.json, so edits to the font need shots fonts again. Ask the owner for the license ('OFL-1.1', 'proprietary', ...) and put it in license; shots check warns without one.
  5. Style and scenes. shots.style: game (sunburst, sparkles, outlined titles, badges) or app (calm gradient, one clean device frame, headline + subline, no sparkles; badges only as quiet chips when the copy has them). Default: app for the app / subscription-app profiles, game otherwise. shots.scenes: palette (game: fire, violet, blast, green, night, sky, gold, candy; app: mist, paper, mint, sand, rose, sky, ocean, slate; or custom { bg, glow, ink, shadow }), frames (one front phone + optional back phones with x / dy / scale / rotate), badge slots (y, side, icon, hot, stars). Fonts default to Paytone One + Montserrat (game) or Inter (app): all cover Latin Extended and Vietnamese. Custom palette bg: ['#a', '#b', '#c'] (or two stops), one colour '#15171C' (flat) or a CSS gradient string 'linear-gradient(160deg, #0b1736, #1d4ed8)' (passed through). Anything else fails config validation and render ("palette.bg must be a colour, a gradient or an array of 2-3 colours"); a dark bg needs a light ink. Photo background (app style): palette.image: 'store/art/hero.png' (+ overlay, 0-0.9, default 0.45: a flat dark layer that keeps white ink legible) on a scene palette, or shots.feature.background: 'store/art/hero.png' | { image, overlay } for the Play feature graphic only. The image is cropped to cover; shots check errors on a missing file and warns when it is smaller than the output (1024 x 500 for the feature graphic, the biggest screenshot size for scenes). Play's feature graphic may be a branded photo (keep text out of it apart from the name); the App Store has no feature graphic, but the same art can be the Header and Search Results asset below. Title weight: shots fonts downloads the weights the templates use (app: 700 / 750 titles, 400-700 body): the full wght axis of a variable Google family, else the range the templates use, else the static weights one by one. A family that lacks a weight is drawn at the nearest one (never faked bolder), so shots fonts warns, and shots check warns per image when the title's weight was not served (font-weight). A local font file declares its weight with { family, file, weight }; a static file needs it.
  6. Render. (--concurrency N, default min(4, cores / 2) pages) storebound shots render --lang en first, review store/renders/en/sheet.png (contact sheet, sizes packed into a grid), then all languages in the background: storebound shots render > .storebound/render.log 2>&1 &.
  7. JPEG for Play. storebound shots jpeg (q92, about 4x smaller than PNG; App Store stays PNG).
  8. Check. storebound shots check (read-only, exit 1 on errors): pixel sizes (Apple: a listed size, portrait or landscape; Play screenshots 320-3840 px at most 2:1; feature graphic exactly 1024 x 500), alpha, Play JPEG / 24-bit PNG and 8 MB, counts per set (Apple 1-10, Play 2-8), text overflow flags (warnings), glyphs drawn by an automatic font fallback or a system font (errors; shots render measures every character in the page), a title drawn at another weight than asked (font-weight, warning), title ink with low contrast against the pixels behind it (contrast, under 3:1, warning: catches white on white), photo backgrounds missing or too small, a device frame cut off by the feature graphic's edge (error unless shots.feature.fit: 'bleed'; the default 'contain' fits the rotated phone with a 24 px margin, scale 0.5-1.5 resizes it) or a screenshot's front device running off a side or the top (warning; the bottom bleed is the design). --variants also checks A/B variant folders. storebound validate runs the same checks on the renders folder and lists errors as blockers next to the store state (--no-local skips them).
  9. Review. storebound shots gallery writes <renders>/index.html: one self-contained page (no network, open it from disk) with every language x size x scene, app previews with posters, variant tabs, filters (language, size, only issues, search) and light / dark. Tiles with store check errors are outlined red, warnings amber, with the message under the tile. Show the owner this page (or a screenshot of it) before uploading.
  10. Upload. storebound play images and storebound apple screenshots (both idempotent, background + log). play images uploads only the sets that exist (tablet / TV / Wear when rendered) and never deletes a set Play has but the renders lack: the dry run shows keeps <type> on Play; --prune (only on the owner's word) deletes them.

Signed-in apps

Key screens behind a login (source: 'web'): capture against a local or demo backend with fabricated data and a demo account. Never real users' data in store screenshots. The same frames feed both stores (App Store and Google Play), so one sign-in setup covers both. Two ways to sign in, set in shots.capture.auth:

  • Sign-in request: { request: { url: '/api/login', method: 'POST', body: { email: '${cred:email}', password: '${cred:password}' } } } with captureAuth: { email, password } in .storebound/credentials.json. It runs once per browser context before its first shot, and the context keeps the cookies. ${cred:NAME} reads captureAuth.NAME (or reviewDemoAccount.username / .password, also from REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD). Keep the two apart: reviewDemoAccount is the account apple review-details sends to App Review and must work on the live backend; a local demo stack's sign-in belongs in captureAuth. check warns when a capture against localhost uses reviewDemoAccount. ${env:NAME} reads the environment. A password-, token- or key-like field with a literal value is refused. A status outside 2xx/3xx stops the capture and shows the status, never the values.
  • Saved session: { storageState: '.storebound/auth.json' }, made by storebound shots login [--url <url>]. It opens a visible browser with a fresh, empty profile. The owner signs in to the demo account by hand (signing in is the owner's step; the agent never types it). Enter in the terminal (or closing the window) saves the state with 0600 permissions and prints how many cookies and origins it saved. The wait times out after 15 min (--timeout <s>) and logs a heartbeat every 30 s. This one is for SSO, 2FA or captcha logins. MCP has no login tool, because the owner signs in at a terminal.

If a shot lands on a sign-in page anyway, capture warns <url> is a sign-in page: session expired? run storebound shots login again and fails that shot at once. auth.loginUrl (a regex) names the login page when the default (login / signin / auth in the path) does not match. Resolved values and cookie values are masked in logs and .storebound/capture-report.json; the gallery only shows the renders.

A/B variants and store experiments

shots.variants.<id>: scenes (order / subset), copy (a JSON merged over the base copy, only what changes), palettes (per scene). storebound shots render --variant b (or --variants for all) writes <renders>/b/<lang>/...; shots check --variants, shots gallery (tabs) and shots jpeg --variant b cover them. play images --variant b / apple screenshots --variant b upload a variant to the MAIN listing (replacing the base images there), so only do that after a test has a winner. What the stores offer, honestly:

  • Google Play store listing experiments (Play Console > Grow users > Store listing experiments): up to 2 variants against the current listing; one default-graphics experiment or up to 5 localized experiments at a time; icon, feature graphic, screenshots (+ descriptions for localized ones); stops automatically after 6 months. Console only: the Play Developer API has no experiments endpoint, so storebound renders and checks the variant images and the agent uploads them in the experiment form with its browser tool; starting the experiment is the owner's button. https://support.google.com/googleplay/android-developer/answer/6227309
  • App Store Product Page Optimization (App Store Connect > your app > Product Page Optimization): up to 3 treatments against the original, one test at a time, up to 90 days; icon, screenshots and app previews, localized per treatment; new metadata goes through App Review; alternate icons must ship in the binary; shown on iOS / iPadOS 15+. https://developer.apple.com/app-store/product-page-optimization/ Through the API with storebound (appStoreVersionExperiments v2, treatments, treatment localizations and their screenshot sets):
    1. storebound shots render --variant b (and shots check --variants, shots gallery).
    2. storebound apple ppo create --variant b --traffic 30 --dry, then without --dry: one test with one treatment per variant (named after it, max 3 variants: --variant b,c) and a localization per App Store locale.
    3. storebound apple ppo upload --variant b --dry, then for real: the variant renders into the treatment's screenshot sets per locale (MD5-idempotent). MCP: apple_ppo_create, apple_ppo_upload, apple_ppo_status.
    4. Owner: storebound apple ppo start (asks first; treatments that are not approved yet go to App Review with the next version), later storebound apple ppo stop (final: a stopped test cannot be restarted; stopping through the API is started: false, not verified against a live test, so if it is refused stop it in App Store Connect). Neither is an MCP tool. Results per treatment are only in App Store Connect. The app must be live (Ready for Distribution); alternate icons and app preview treatments stay App Store Connect clicks.
  • Test one thing per experiment (usually the first screenshot or the order), give it real traffic, and do not read results early.

Header and Search Results (App Store creative assets, iOS / iPadOS 27+)

Since 5 Oct 2026 the App Store version page has a "Header and Search Results" tab next to "App Previews and Screenshots". Optional: without these assets the App Store uses in-app events, app previews and screenshots.

  • Files under the renders folder, per language: <renders>/<lang>/creative/header.png (3840 x 1646, 21:9), search.png (3:2, 1920 x 1280 up to 3840 x 2560), or one universal.png (5244 x 2950, 16:9) that fills either slot (also <renders>/creative/universal.png for every locale, or apple.creativeAssets.universal). Art without words needs no copy per locale: <renders>/creative/header.png / search.png fill every language without its own file. JPEG / PNG without alpha. Videos instead: header.mov|m4v|mp4 / search.*, 5-30 s, 30 or 60 fps, the same aspect ratio; search videos play muted. apple.creativeAssets.header: false / search: false skips a slot.
  • Not rendered by shots render: the scene templates are portrait device shots; make the wide art yourself (or from the feature graphic artwork at the larger size) and drop it in.
  • Upload and select: storebound apple creative-assets --dry, then without --dry. Files go into the app's Asset Library (API: appAssetLibraryImages / appAssetLibraryVideos, category CREATIVE_ASSETS; processing is a bounded wait of 2 minutes with a heartbeat). The version page's slots offer only approved assets ("Browse approved assets"), so an asset is placed (appAssetLibraryPlacements, PRODUCT_PAGE_HEADER_ASSET / APP_STORE_SEARCH_RESULTS_ASSET) once Apple approved it; until then each locale and placement says "uploaded , waiting for Apple's approval; run storebound apple creative-assets again later", and a later run places it. --verify, validate and the report show the same waiting state. Same file (MD5) = nothing sent; an archived copy is unarchived; a file App Review rejected is never uploaded again (change the file). MCP: apple_creative_assets (dry by default).
  • Review: an asset is reviewed only once it is in a review submission (its own item, appAssetLibraryImage / appAssetLibraryVideo; the console's "Add for Review"). --add-for-review adds the uploaded, unreviewed assets to the app's draft submission, once: before the first approved version it is the same submission as that version, later they can go alone. Pressing Submit for Review stays the owner's.
  • Live app, no editable version: the run uses the live version, where only APPROVED assets are selected (Apple swaps approved assets onto a live version without a new build). New files are uploaded and wait for review: --add-for-review sends them as a submission of their own (the app has an approved version), the owner presses Submit for Review, and a re-run after approval selects them.
  • Verify: storebound apple creative-assets --verify (MCP apple_creative_assets_verify) is read-only: every slot of the editable version (else the live one) shows the local file, with its review state; exit 1 when one differs.
  • Checks: storebound check and validate print an info note when no locale has one.
  • Play: no such slot. The feature graphic (1024 x 500, play images) and the promo video (YouTube URL) are the closest equivalents.

From an AI tool (MCP)

storebound mcp exposes shots_check (read-only) and shots_capture, shots_render, shots_gallery (local files, dry_run: true by default: run it once dry, show the plan, then with dry_run: false). Uploads (apple_screenshots, play_images, both with variant) are store writes: dry first, and only for real after the owner agreed. Rendering many languages takes minutes: in a terminal prefer storebound shots render > .storebound/render.log 2>&1 &.

Gotchas

  • shots render's contact sheet (sheet.png) waits for every image to decode: its timeout grows with the images and sizes on the sheet and with the machine's load (max(1-min, 5-min load) / cores, 1-5x), with a "still loading" line every 15 s. A timeout names the sheet, the budget and the page errors. --no-sheet skips it.
  • Output is 24-bit RGB PNG: Play rejects alpha / palette PNGs, Apple rejects alpha.
  • Arabic: rtl: true on the language; numbers stay LTR inside RTL text automatically.
  • Japanese breaks by phrase, Korean at spaces; long words shrink the line rather than wrap.
  • Check every language's sheet for overflow before uploading; German / Russian titles run long.
  • Play: 8 per type max; Apple: 10 per set max.
  • Do not include prices, "free" badges for paid items, or other platforms' UI.