App Store Connect just sent the rejection email with ITMS-90683 in the subject line, and the offending key is NSPhotoLibraryUsageDescription. The build compiled cleanly, the simulator never asked for photo permission, and the upload finished without complaint. Now the binary sits in the Invalid Binary state and the release is on hold.
Short answer
ITMS-90683 with NSPhotoLibraryUsageDescription is a static-analysis rejection from App Store Connect. The validator scans your IPA, finds at least one symbol that links against the Photo Library APIs, and cannot find a matching purpose string in Info.plist. The fix is one key with one honest sentence, plus a new build number before resubmission. The offending symbol often comes from a third-party SDK rather than your own code.
What you should know
- The validator inspects symbols in the compiled binary, not runtime behavior. Any linked framework that references PhotoKit, AssetsLibrary, PHPickerViewController, or UIImagePickerController is enough to trigger the rejection, even when the user-facing code never opens the photo picker.
- Apple has required purpose strings for the photo library since iOS 10.0. Apps that try to read or write the camera roll without NSPhotoLibraryUsageDescription crash on first access, and uploads to App Store Connect fail validation before App Review ever sees the binary.
- Empty strings do not pass the validator or the runtime. The key has to carry a non-empty human-readable value. iOS treats an empty string as undefined behavior at the permission prompt.
- Two related keys cover different access patterns. NSPhotoLibraryUsageDescription is the read-and-write key, available since iOS 6.0. NSPhotoLibraryAddUsageDescription, introduced in iOS 11.0, is write-only and shows a softer prompt.
- Editing Info.plist by hand frequently makes the rejection worse. A misplaced bracket or duplicate entry causes the validator to treat the key as absent, which loops developers back to the same email.
- Resubmission requires a new build number. App Store Connect refuses to accept the same CFBundleVersion twice within a release, regardless of which key you added.
Why does App Store Connect block a build with ITMS-90683?
The short answer is that ITMS-90683 is a static check, not a behavioral one. When you upload an IPA through Xcode, Transporter, or altool, App Store Connect runs a server-side validator that inspects every symbol in the compiled binary and every framework you ship. If any of those symbols match the list of APIs Apple has flagged as sensitive for the photo library, the validator looks for the matching purpose string key in Info.plist. When it is missing, the build moves to the Invalid Binary state and you receive the ITMS-90683 email with NSPhotoLibraryUsageDescription named in the body.
The enforcement is grounded in Apple's privacy guidance for iOS apps, which states that an iOS app linked on or after iOS 10.0 must statically declare its intent to access the photo library. The check runs before the binary reaches a human reviewer, which is why some teams see the rejection within minutes of the upload while the rest of the submission is still processing. Static analysis means the validator does not care whether the code path ever runs at runtime. The symbol on disk is enough.
Which APIs and frameworks trigger the requirement?
Four common API surfaces pull the NSPhotoLibraryUsageDescription requirement into a build. PhotoKit, the modern interface that lives in the Photos and PhotosUI frameworks, is the largest source. UIImagePickerController in UIKit triggers the same gate, because the picker reads from the camera roll. PHPickerViewController, introduced in iOS 14 as the limited-access alternative, also touches the photo library symbol space. The older AssetsLibrary framework, deprecated since iOS 9, still triggers a match when a legacy SDK ships with it.
Vendor SDKs are the usual cause. Analytics frameworks that capture user feedback with attached screenshots, share extensions that present a system share sheet, social SDKs that paste a card to a story, customer-support widgets with photo upload, and AR SDKs that load reference images all link Photo Library symbols. A GitHub issue on the FlutterFlow tracker documents this pattern on builder platforms, where the developer never wrote a line of Swift but the bundled plugins added PhotoKit references during the iOS build.
To find the source in your own binary, run nm against the executable inside the .app folder and grep for Photo, PHAsset, ALAsset, or UIImagePicker. The output names the framework and the specific symbol. That tells you whether the reference is yours or comes from a SwiftPM dependency, a CocoaPod, or a vendored framework.
How do you add NSPhotoLibraryUsageDescription correctly in Xcode?
The cleanest path uses the Info tab of the project editor rather than the raw plist file. Open the project in Xcode, select the target, open the Info tab, click the plus icon under Custom iOS Target Properties, and start typing the key name. Xcode autocompletes to "Privacy - Photo Library Usage Description" and writes the underlying XML correctly. Set the value to one sentence describing why your app reads photos, save, and rebuild.
Developers on the Apple Developer Forums repeatedly note that direct edits to Info.plist in a text editor cause silent corruption: the indentation looks fine to a human, but the binary plist encoder produces a structure the validator treats as missing. If you must edit the XML by hand, run plutil -lint ios/App/Info.plist after every change to confirm the file parses cleanly. Then increment CFBundleVersion in the same build before re-archiving, otherwise App Store Connect rejects the upload with a separate error about the duplicate build number.
For builders who want an external automated pass over the build before resubmission, PTKD.com (https://ptkd.com) is one of the platforms focused on scanning compiled IPA and AAB files for missing privacy declarations and other OWASP MASVS findings. The goal is to catch the gap on a local machine, not on Apple's servers, where each rejection costs another review cycle.
What is the difference between NSPhotoLibraryUsageDescription and NSPhotoLibraryAddUsageDescription?
The two keys cover different access levels and were introduced in different iOS versions. NSPhotoLibraryUsageDescription is the broader read-and-write key. NSPhotoLibraryAddUsageDescription is the narrower write-only key. If your app only saves an image to the camera roll, the Add variant is enough. If your app reads any existing photo, opens an image picker, or scans for assets, the Usage variant is required. Apps that both read and write declare both keys.
| Key | iOS minimum | Access scope | When to use |
|---|---|---|---|
| NSPhotoLibraryUsageDescription | iOS 6.0 | Read and write | App opens the image picker, reads existing photos, or scans for assets |
| NSPhotoLibraryAddUsageDescription | iOS 11.0 | Write only | App exports a generated image, saves a video, or adds a downloaded photo |
| Both keys present | iOS 11.0 | Read and write, softer write prompt | App reads existing photos and also saves generated content |
| Neither key with Photo Library symbols | n/a | Runtime crash; binary rejected with ITMS-90683 | Never the correct state |
The system displays the broader prompt first when the app calls the read path. The Add-only prompt presents a smaller dialog and skips the Limited Access picker that appears for full library access. Picking the right key reduces friction in the permission flow and lets the user understand the actual scope of the request.
How do cross-platform projects expose the photo library key?
Every framework that produces an iOS binary goes through the same App Store Connect validator, so every framework needs the same key. The configuration surface differs by tool.
- Expo writes Info.plist from the
ios.infoPlistsection ofapp.jsonorapp.config.js. Per Expo's configuration documentation, add"NSPhotoLibraryUsageDescription": "..."inside that object and runnpx expo prebuild --cleanto regenerate the native project. - React Native keeps Info.plist at
ios/[ProjectName]/Info.plist. Add the key through Xcode rather than a text editor. - Flutter uses
ios/Runner/Info.plist. Edit it through Xcode by openingios/Runner.xcworkspaceand selecting the Runner target's Info tab. - FlutterFlow exposes a Custom Info.plist Entries section under App Settings, Permissions. Add the key name and the value there; the next iOS build includes it.
- Capacitor edits
ios/App/App/Info.plistdirectly through Xcode.
The destination file is the same Info.plist in every case. The validator does not care which tool wrote the entry as long as the XML is well-formed.
What kind of purpose string does Apple actually accept?
The automated ITMS-90683 check passes any non-empty string, but a human reviewer in App Review can still flag a vague description under Guideline 5.1.1 of the App Store Review Guidelines. Strings like "Used for photos", "App needs access", or a single brand name regularly draw a manual rejection during review.
A purpose string that clears both gates names the feature and the user benefit in one sentence. Examples that pass:
- "Lets you attach a photo from your camera roll to a chat message."
- "Used to set a profile picture from photos already on your device."
- "Saves the receipts you generate inside the app to your camera roll."
Examples that risk a manual review rejection:
- "Photos."
- "App requires this permission."
- "Will use to make app better."
The same string is displayed inside the system permission alert at runtime, so a clear sentence does double duty: it clears the App Review gate and earns more Allow taps from real users.
What to watch out for
Several traps surface during the fix and the resubmission. The first is forgetting to increment CFBundleVersion: the validator rejects the upload before it ever checks the new key, and the developer concludes that the purpose string did not take. Increment the build before every archive.
The second is adding NSPhotoLibraryUsageDescription only to the main target and not to extensions such as a Share Extension or a Notification Service Extension when those extensions also link Photo Library frameworks. Each extension carries its own Info.plist.
The third is treating an SDK update as the cause of the rejection. Many teams add the key, ship one release, then remove the key when they audit dependencies, only to see the rejection return on a later build when a different SDK update reintroduces the symbol. The safer pattern is to keep the key in place once it has been added, because the runtime alert only fires when an API path actually requests access.
The fourth is removing the key as a privacy-conscious move without removing the SDK that pulls it in. The result is a runtime crash the first time any code path touches Photo Library APIs, often on a user device rather than in your testing.
Key takeaways
- Treat ITMS-90683 as a static-analysis result, not a behavioral one. The validator scans symbols in the compiled binary, so the fix lives at the Info.plist layer regardless of whether your own code uses the photo library.
- Pick the narrow key when you can. NSPhotoLibraryAddUsageDescription is enough for apps that only export images, and it shows a less alarming prompt than the full read-and-write key.
- Write a specific purpose string the first time. Vague descriptions clear the automated validator but get flagged in human review, which costs another full review cycle and another build number.
- Increment the build number before every resubmission. Otherwise the new key never gets the chance to clear the gate.
- Some teams outsource pre-submission validation to platforms like PTKD.com (https://ptkd.com), which scans compiled IPA and AAB files for missing privacy declarations and other OWASP MASVS findings before the build is uploaded to App Store Connect.




