I built a sticker pack. Thirty fishing puns, no code, two targets — the structure Xcode generates for you when you pick “Sticker Pack App” out of the template picker. It is the smallest shippable thing on the platform.
It took eight failed uploads and most of a day to get it into App Store Connect, and the fix was to stop being a sticker pack.
Every upload came back the same way. HTTP 409, STATE_ERROR.VALIDATION_ERROR:
Missing app icon. iMessage-only apps must include an asset catalog with a large app icon to be submitted for review. Manually add a large app icon as a 1024 by 1024 pixel PNG and resubmit.
The diagnosis writes itself. Add a 1024×1024 icon. I have one — it’s sitting in the repo, it’s the icon on the App Store listing, it’s a flexing largemouth bass in aviators. The error even tells you the dimensions.
The problem is that there is nowhere to put it.
The Format Has No Slot
An iMessage sticker pack uses a .stickersiconset, not the .appiconset a
normal iOS app uses. Different schema, different slots. Open Xcode 26.2’s own
“Sticker Pack App” template and the entire icon inventory is:
29×29 60×45 67×50 74×55 27×20 32×24 1024×768
That’s it. The largest entry is 1024×768 — the Messages App Store banner, a 4:3 image. There is no square 1024. Not one that’s empty; one that does not exist in the schema.
So I added it by hand. actool dropped it silently — no warning, no error,
just an inventory that came out the other side without it. I tried the entry
as ios-marketing, as ios-marketing with an explicit platform:ios, as
universal with platform:ios. Dropped every time.
Then I added a separate .appiconset named “iMessage App Icon” alongside the
sticker icon set, which is roughly what the error message describes. actool
exited 255 with no diagnostics at all. Not an error message — a crash.
At this point the check is asking for an artifact that the toolchain will not produce and the format has no room for.
Four Configurations, All Rejected
I stopped guessing and started enumerating. Each of these is a full archive and a full upload:
- Template-exact. No square 1024 anywhere, because the template has no slot for one. → 409.
- Host app given its own
AppIcon.appiconset, modern single-size 1024 in the “Any Appearance” well. → 409. - Same, in legacy multi-size format with an explicit
ios-marketing1024×1024,CFBundleIconNamepresent. I verified the compiledAssets.carwithassetutiland the opaque 1024×1024Icon Imagewas genuinely in there. → 409. - Everything, everywhere.
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS=YESso both the app’s and the extension’sAssets.carcarried anAppIcon1024×1024. A loose 1024×1024 PNG in both bundles. Both registered inCFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles.CFBundleIconNamein both Info.plists. Verified withassetutil. → 409, and every other icon check passed.
Configuration 4 is the one that hurt. There is no remaining interpretation of “include a 1024×1024 app icon” that build does not satisfy.
The Configuration That Explained It
Then I did something slightly perverse. I switched the extension’s primary
icon to a regular AppIcon.appiconset containing the 1024, letting actool
auto-register the loose AppIcon1024x1024.png.
The response came back with nine errors instead of one. The original 1024 complaint, plus eight new ones demanding the sticker drawer renditions I had just displaced:
54×40 81×60 64×48 96×72 120×90 134×100 148×110 180×135
That’s the most useful failure I got all day. It proves the validator reads the extension’s icon inventory accurately and reports on it in detail. The server knows exactly what’s in the bundle. It is not confused about my assets. Configuration 4 satisfied every check it has except the one it wouldn’t stop raising.
What the Server Was Actually Reading
Buried in ContentDelivery.framework there’s a tool called swinfo. It’s
what the upload pipeline itself runs to produce the ASSET_DESCRIPTION that
the server validates against. You can run it on your own IPA and see the exact
document the validator will see:
ContentDelivery.framework/Resources/swinfo -f <ipa>
When a server-side check rejects your binary, the argument isn’t really about your Xcode settings. It’s about the asset description generated from your bundle, and until you can read that document you are arguing about it blind.
For configuration 4, swinfo confirmed both bundles’ icon inventories
contained a 1024×1024 entry. The thing being demanded was present in the
document being validated, and the validation failed anyway.
The control that settled it: Feastmark, a normal SwiftUI app from the same
team, same machine, same Xcode. It uploads fine. Its swinfo icon inventory
contains no 1024×1024 either.
So the check isn’t “does this bundle have a large icon.” It’s a code path that
only runs for application.messages products, and on that path it cannot be
satisfied by anything Xcode 26.2 will build.
The Fix Was to Stop Qualifying
The error says iMessage-only apps must include the icon. So I made it not an iMessage-only app.
The host target went from com.apple.product-type.application.messages — the
code-less stub Xcode generates — to com.apple.product-type.application, a
regular iOS app. I gave it a real SwiftUI screen: a gallery that reads the
sticker PNGs out of the embedded extension at runtime and displays the pack.
Maybe eighty lines. The sticker extension itself I did not touch.
The next upload went through.
It isn’t a workaround for the bug so much as a way of no longer being the kind of app the bug applies to, and it isn’t free:
- The app now has a home-screen icon, because it’s now an app.
- The App Store Connect primary category had to become Entertainment. The Stickers category is reserved for iMessage-only apps — the exact classification I’d just abandoned.
- The iOS screenshot slot became the one that mattered.
- There’s now a UI to maintain on something I’d designed to have none.
For a pack of thirty fishing jokes, that’s a real amount of collateral change to route around a validator.
The Dead End I Spent Two Hours On
Between configurations 3 and 4 I convinced myself the answer was to hand-write
the icon registration. swinfo said the server reads
CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles, so I declared it myself
and dropped a loose iMessage App Icon1024x1024.png into the bundle next to it.
It doesn’t take. actool emits a partial Info.plist that gets merged over
yours, and it overwrites CFBundleIcons wholesale. A static plist entry
loses. A build script phase either loses too or introduces a dependency cycle
trying to run late enough to win.
The only thing that survives is patching the archive after archiving, before distribution — Organizer re-signs during export, so the edit holds:
A=<archive>/Products/Applications/ReelTalk.app
/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles: string 'iMessage App Icon1024x1024'" "$A/Info.plist"
cp "ReelTalk/HostAssets/iMessage App Icon1024x1024.png" "$A/"
None of which worked. That’s configuration 4 — swinfo confirmed the 1024 was
in the inventory and the upload was rejected anyway. I’m keeping it here
because the actool-overwrites-your-plist behavior is real and will cost
somebody an evening. But I’d been handed a true fact about what the server
reads, and I spent two hours using it to build a more and more elaborate
answer to a question nobody had asked.
Once the host became a regular app, all of it evaporated. A normal app has a
normal .appiconset, actool generates CFBundleIcons correctly on its own,
and the hand-written plist block came back out of the project in the same
commit that added the SwiftUI gallery. The shipping build patches nothing.
What I’d Take From This
Three of my eight uploads went into satisfying a sentence instead of asking whether the sentence could be satisfied. “Manually add a large app icon as a 1024 by 1024 pixel PNG” names a size, a format, and an action, and I took all three as facts about my project rather than as a string somebody typed into a validator at some point and nobody revisited.
An error message is a claim about your bundle. It is not evidence that the
claim is checkable. The template had no square slot and actool refused to
emit one. Both of those were knowable in the first ten minutes, from Xcode,
without uploading anything. I went and confirmed them after the fourth
rejection.
swinfo is the thing I’d reach for sooner next time. It sits in
ContentDelivery.framework, it prints the exact document the server validates
against, and it was there the entire time I was guessing. Once I could read
that, the question stopped being what does Apple want and became what is
Apple reading — and the second one you can answer in a terminal instead of by
building an archive and waiting on a 409.
The change that finally shipped it is one SwiftUI file, about eighty lines, displaying a grid of stickers. Nobody who buys a sticker pack is ever going to open that screen. It exists so the binary would stop being the kind of thing the broken check runs against.
Filed with Apple Developer Support as case 20000119974759. As far as I know the stub path is still broken for everyone else.