Blog · September 13, 2026

The screenshot upload that said “done” and delivered nothing.

We built a publish button that sends a finished screenshot set straight from the editor to App Store Connect. Every request came back green: the reservation returned 201, each chunk upload returned 200, the commit returned 200, and the results panel put a check mark next to every image. The listing showed nothing. Four separate bugs had lined up behind that check mark, and the last one is the reason the first three went unnoticed.

What “uploaded” means to App Store Connect

Uploading a screenshot through the App Store Connect API is a three-step handshake. You POST a reservation naming the file and its size, and Apple answers with a list of upload operations: presigned URLs, byte offsets, chunk lengths. You PUT the bytes to those URLs. Then you PATCH the screenshot record with uploaded: true and an MD5 of the file so Apple can confirm nothing was lost on the way.

Each step returns a 2xx status when it succeeds, and each step is only about bytes. None of them looks at the pixels. Validation happens afterwards, asynchronously, and the verdict lands in a field on the screenshot record called assetDeliveryState. Its state moves from AWAITING_UPLOAD to UPLOAD_COMPLETE and then to either COMPLETE or FAILED, and a failure comes with an errors array that says why. We read the status code of the commit and stopped there. Everything below follows from that.

Bug one: the PNG that was never opaque

Apple's screenshot specification has a line that reads like boilerplate: images can't include alpha channels or transparencies. Our editor renders each screenshot to a browser canvas and exports it as PNG, and a canvas PNG is always RGBA. Every pixel was fully opaque. The channel still existed, and Apple rejects the channel, not the transparency. Google Play is just as blunt; its requirement reads “JPEG or 24-bit PNG (no alpha)”.

The fix was to stop trusting the exporter. Every image is now redrawn onto a fresh opaque canvas and re-encoded as JPEG before it leaves the browser, because JPEG has no alpha channel to get wrong. On the mobile app we went a step further and read the PNG header before any upload: byte 25 of a PNG is its colour type, and values 4 and 6 mean an alpha channel is present. That check runs in the compliance sheet, so a transparent export is caught before a single request is made.

Bug two: pixels that were “about” the right size

Apple files every screenshot into a display class by exact dimensions. 1290×2796 is a 6.7-inch iPhone; 1289×2796 is nothing, and it gets rejected. Some of our exports weren't landing on the exact target size. The same opaque-canvas re-encode fixed this for free, because it draws into a canvas created at the target width and height rather than trusting whatever size the renderer produced.

Bug three: the 409 that was really a full set

Once the images were valid, the reservation step started failing with HTTP 409 Conflict. A conflict on a reservation is not a helpful message, and it took a while to see the cause: every earlier failed upload was still sitting in the screenshot set. A set holds ten images, a failed image keeps its slot until you delete it, and our test runs had filled every slot with rejected files.

There was a quieter contributor. Our “6.5-inch” export target was 1284×2778, which is really a 6.7-inch size, so those images landed in the same set as the 6.9-inch ones (Apple files 1320×2868 and 1290×2796 in that class too). The true 6.5-inch size is 1242×2688, and it gets its own set. Two targets sharing one bucket overflowed it faster.

The publisher now treats an upload as a replacement, not an append. It clears the set once per run before uploading into it, refuses an eleventh image with a plain message instead of letting Apple answer with a 409, and retries a reservation once, because a slot freed a moment earlier can lag.

Bug four: the success report

This is the one that let the others hide. After the commit returned 200 we told the user the screenshot was published. Apple, a second or two later, was marking that same screenshot FAILED with an error description naming the alpha channel or the dimensions, and nobody was listening.

The publisher now polls each screenshot's delivery state after the commit. Format and dimension failures surface within seconds, so the poll is short: a handful of reads about two seconds apart, run in parallel across the batch. COMPLETE is reported as success. FAILED is reported with Apple's own description, verbatim, not paraphrased. An image still processing when the budget runs out is reported as delivered, because by then the bytes are in and no rejection has happened. The results panel changed from a wall of green to a count of what landed, a count of what didn't, and each distinct reason listed once.

Google's model is the mirror image

Google Play doesn't have the asynchronous-verdict problem, but it has its own way of making an upload look finished when it isn't. The Play Developer API works in edits: open an edit, delete the existing images in a bucket, upload the new ones, commit. Google's documentation is explicit that changes made within an edit are not live until the edit is committed, that each user may have only one edit open at a time, and that opening a new edit invalidates the old one. A publisher that uploads and forgets to commit has done nothing visible. A publisher that opens a fresh edit for each image throws away the previous image's work.

So the two stores fail in opposite directions. Apple accepts your bytes and rejects them later; Google holds your bytes in limbo until you say the word. Either way, the HTTP status of the upload call is not the thing to show the user.

If you automate store uploads: after the commit, read assetDeliveryState on Apple and actually commit the edit on Google. Pass the store's error text through unchanged. Flatten to opaque, exact-size images before any bytes leave the device.

The rule we kept

For any store API, the status code tells you the bytes arrived. Something else, a field or a commit, tells you whether the store accepted them. Read that before you say anything to the user, and when the store gives you a reason, show it as written. “Couldn't publish” is a support ticket; a message that names the alpha channel is a fix.

The other half is to catch what you can before the bytes leave. The alpha check, the exact-dimension check, and the ten-per-set count all run in ShotCanvas before an upload starts, in the store guidelines card on the export screen. The App Store still gets the last word. It just has fewer things left to say.

Publish to both stores from Studio Every exact screenshot size