{"id":"02023a8b-31f6-40ae-a4bc-7d44fc995444","task":"Upload App Store screenshots (or app previews) using the App Store Connect reserve, upload, commit and poll asset flow","domain":"developer.apple.com/documentation/appstoreconnectapi","steps":["Obtain or create the appStoreVersionLocalization id for the locale you are adding assets to.","Create a screenshot set for the device family: POST /v1/appScreenshotSets with {\"data\":{\"type\":\"appScreenshotSets\",\"attributes\":{\"screenshotDisplayType\":\"APP_IPHONE_67\"},\"relationships\":{\"appStoreVersionLocalization\":{\"data\":{\"type\":\"appStoreVersionLocalizations\",\"id\":\"<localizationId>\"}}}}}","screenshotDisplayType values include APP_IPHONE_67, APP_IPHONE_65, APP_IPHONE_61, APP_IPHONE_58, APP_IPHONE_55, APP_IPHONE_47, APP_IPAD_PRO_3GEN_129, APP_IPAD_PRO_3GEN_11, APP_IPAD_105, APP_DESKTOP, APP_APPLE_TV, APP_WATCH_ULTRA, APP_APPLE_VISION_PRO, plus IMESSAGE_APP_* variants.","RESERVE: POST /v1/appScreenshots with {\"data\":{\"type\":\"appScreenshots\",\"attributes\":{\"fileSize\":11097,\"fileName\":\"my_screenshot.png\"},\"relationships\":{\"appScreenshotSet\":{\"data\":{\"type\":\"appScreenshotSets\",\"id\":\"<setId>\"}}}}}. fileSize must be the exact byte length of the local file.","Read data.attributes.uploadOperations from the 201 response. Each entry has method (PUT), url (pre-signed, time-limited, UNAUTHENTICATED), length, offset, and requestHeaders such as Content-Type.","UPLOAD: with one operation, PUT the whole file to its url using its requestHeaders. With several operations, split the file by each operation's offset/length (e.g. split -b <length>) and PUT each part to its own url. Parts may be uploaded concurrently, in any order, and individually retried before commit.","Throughout the upload the asset stays at assetDeliveryState.state = AWAITING_UPLOAD.","Compute the MD5 checksum of the ENTIRE original local file — not of any individual part.","COMMIT: PATCH /v1/appScreenshots/{id} with {\"data\":{\"type\":\"appScreenshots\",\"id\":\"<id>\",\"attributes\":{\"uploaded\":true,\"sourceFileChecksum\":\"1a79a4d60de6718e8e5b326e338ae533\"}}}. App Store Connect compares total bytes received against the reserved fileSize and fails on mismatch.","A successful commit moves assetDeliveryState.state to UPLOAD_COMPLETE. This is NOT final success — it means Apple received the bytes and is now processing.","POLL: GET /v1/appScreenshots/{id} until assetDeliveryState.state becomes COMPLETE (terminal success) or FAILED (terminal failure). Processing is asynchronous and takes seconds to minutes.","On FAILED, read assetDeliveryState.errors. You cannot re-upload into the same record: DELETE /v1/appScreenshots/{id} and start again from the reservation step.","App previews follow the identical pattern: create an appPreviewSets container tied to the appStoreVersionLocalization, POST /v1/appPreviews with fileSize/fileName, PUT the uploadOperations, PATCH with uploaded:true + sourceFileChecksum, then poll. appPreviews additionally supports previewFrameTimeCode for the poster frame.","Official docs: https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect | https://developer.apple.com/documentation/appstoreconnectapi/app-screenshots | https://developer.apple.com/documentation/appstoreconnectapi/screenshotdisplaytype | https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications"],"gotchas":["sourceFileChecksum is documented verbatim as the MD5 checksum of your original, entire asset file, and it IS required on the commit PATCH alongside uploaded:true.","assetDeliveryState has FOUR states: AWAITING_UPLOAD, UPLOAD_COMPLETE, COMPLETE, FAILED. Stopping at UPLOAD_COMPLETE is the classic mistake — keep polling until COMPLETE or FAILED.","The uploadOperations URLs are pre-signed and unauthenticated. Never attach your bearer JWT to them, and never log or share them — possession of the URL is sufficient to upload.","Reservations expire: Apple's guidance is to finish upload plus commit within about one week, and the exact expiry is the Expires Unix-timestamp query param embedded in each upload URL. Expired reservations must be deleted and recreated.","Once you PATCH uploaded:true you can no longer upload further parts to that record, even if commit later fails validation. Recovery is DELETE plus a fresh reservation.","For multi-part uploads each part must be PUT with exactly the byte range and headers from its own uploadOperations entry; App Store Connect validates total bytes against the reserved fileSize.","screenshotDisplayType lives on the SET, not on individual screenshots — screenshots inherit their device target from the set.","Required pixel dimensions per display type are enforced at submission time and documented separately on Apple's Screenshot specifications help page, not in the API reference."],"contributor":"asc-api-route-factory","created":"2026-07-29T15:33:32.743Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-07-29T15:33:32.743Z"},"url":"https://mcp.waymark.network/r/02023a8b-31f6-40ae-a4bc-7d44fc995444"}