Before you start: what Apple accepts
- PNG or JPEG, RGB, no transparency, at an exact size per device class: 6.9″ iPhone
1320 × 2868, 6.5″ iPhone1284 × 2778, iPad 13″2064 × 2752(or the landscape equivalents). See all sizes. - Up to 10 screenshots per device class, per language.
- You can only change screenshots on a version you can still edit: one that's being prepared, or that was rejected. A version that's live or in review is locked.
1. Drag and drop in App Store Connect
Open your app → the version under iOS App → scroll to Previews and Screenshots, pick the device size and drag your files in. It's fine for one language and one size. It gets slow with several sizes, many languages, or Custom Product Pages, and the browser uploader occasionally stalls on large sets.
2. fastlane deliver
Put screenshots in fastlane/screenshots/<locale>/ and run fastlane deliver. It authenticates with an App Store Connect API key or Apple ID, matches files to device sizes by their pixel dimensions, and uploads metadata too. It's great in CI, but you're maintaining a Ruby toolchain and a folder convention.
fastlane deliver --skip_binary_upload --skip_metadata \
--overwrite_screenshots --api_key_path ./asc_key.json3. The App Store Connect API directly
Apple's API uploads screenshots in three phases: reserve, upload, commit. You need an API key from Users and Access → Integrations → App Store Connect API with the App Manager role, and you sign each request with a short-lived ES256 JWT (aud: appstoreconnect-v1, at most 20 minutes).
- Editable version.
GET /v1/apps/{id}/appStoreVersionsand take one inPREPARE_FOR_SUBMISSION; if there isn't one, create the next version withPOST /v1/appStoreVersions. - Localization.
GET /v1/appStoreVersions/{id}/appStoreVersionLocalizationsand pick e.g.en-US. - Screenshot set. Get the localization's
appScreenshotSetsfiltered byscreenshotDisplayType(APP_IPHONE_67for 6.9″), or create one. - Reserve.
POST /v1/appScreenshotswithfileNameandfileSize. The response includesuploadOperations: URLs, byte ranges and headers. - Upload.
PUTeach byte range to its URL with only the headers Apple gave you. - Commit.
PATCH /v1/appScreenshots/{id}withuploaded: trueand the file's MD5 assourceFileChecksum, then pollassetDeliveryStateuntil it readsCOMPLETE.
To replace a set, delete the existing screenshots in that set first. Other languages and sizes aren't affected.
The errors that trip people up
- 400 on the upload PUT: you sent your
Authorizationheader to Apple's pre-signed upload URL. Send only the operation's own headers. - Screenshots stuck processing or failed: the MD5 in the commit didn't match the bytes, or the image isn't an accepted size.
- 409 when creating a version: a version is already in progress. Use the editable one instead of creating another.
- 403: the key's role can't edit app metadata. Use App Manager or Admin.
- 401 after a while: your JWT expired. Tokens last 20 minutes at most; make a fresh one.
4. One click from Agent C
If your screenshots come from Agent C, you skip all of the above. Connect your App Store Connect API key once (it's encrypted at rest and only used when you send), then press Send to App Store on any unlocked set. Agent C finds your editable version (or offers to create the next one as a draft), replaces that language's screenshots for that device size, and waits for Apple to finish processing. Nothing is ever submitted for review; you check the draft and submit it yourself.
Your AI agent can do it too: with the Agent C MCP server, Claude Code, Cursor or Codex can design the set and call send_to_app_store. Here's a real example: six screenshots into Tiny Legacy's version 1.2.3 draft.