If you uploaded a new iOS build to App Store Connect this morning and the validator came back with ITMS-90683 next to your version number, the build did not fail for a code signing or metadata reason. It failed because Apple's automated scan found a call (or a link reference) to a Photo Library API in your binary without a matching NSPhotoLibraryUsageDescription string in your Info.plist. The fix is a single key, but identifying which SDK pulled in the API is usually half the work.
Short answer
ITMS-90683 means your compiled iOS binary references the Photo Library APIs without a corresponding NSPhotoLibraryUsageDescription string in Info.plist. The fix is to add the key with a short, honest sentence explaining why the app reads photos, rebuild, and resubmit through App Store Connect. The reference often comes from a third-party SDK (image picker, share plugin, social login, AdMob) rather than your own code, so audit your dependencies before you write the description. According to Apple's NSPhotoLibraryUsageDescription documentation, any iOS app linked on or after iOS 10.0 that accesses the user's photo library must statically declare the intent by including this key in Info.plist with a purpose string.
What you should know
- The check is static, not runtime. App Store Connect scans the compiled binary at upload, so removing the call at runtime does not help. The symbol still appears in the linked frameworks.
- The purpose string is shown to users. When iOS presents the permission prompt, it displays the exact text from your NSPhotoLibraryUsageDescription value, so write something a human would understand.
- Add-only access has its own key. If the app only writes to the camera roll, use NSPhotoLibraryAddUsageDescription instead of the read key.
- The trigger is often a transitive SDK. Many builds fail ITMS-90683 even when the app code never touches PhotoKit, because an analytics, ad, or social SDK references the API internally.
- Generic placeholder strings can re-trigger human review. A description like "Allow access to photos" passes the validator but can earn a Guideline 5.1.1 push-back once a reviewer reads the metadata.
What is ITMS-90683 and why does App Store Connect raise it?
The short answer is that ITMS-90683 is the validator code for a missing purpose string. App Store Connect scans every binary you upload, looks for symbols that map to privacy-sensitive APIs, and cross-references them against the keys in your Info.plist. If a symbol references PhotoKit, AssetsLibrary, UIImagePickerController, or related photo APIs and no NSPhotoLibraryUsageDescription key is present, the validator stops the build and emits ITMS-90683.
The scan happens after Xcode finishes uploading the IPA, before the build appears in TestFlight or the Activity tab. That timing matters: the build never shows up as a candidate version, so you cannot ship it as a hotfix until the warning is resolved. The same scan covers other privacy APIs (camera, microphone, location, contacts, calendar), each tied to its own purpose-string key. The validator reports one missing key at a time, which is why builds often fail ITMS-90683 twice in a row, first for the photo library and then for the camera.
Apple expanded the strictness of this check around 2018 to catch builds where SDKs silently called Photo Library APIs in code paths the developer believed were disabled. According to a long-running Apple Developer Forums thread on the missing Info.plist key, the check is symbol-based, not control-flow based, so dead code that links against the API still triggers it.
How do you decide between NSPhotoLibraryUsageDescription and NSPhotoLibraryAddUsageDescription?
The short answer is by how the app actually uses the photo library. The two keys cover different scopes, and choosing the wrong one is one of the more common reasons a re-submitted build re-trips ITMS-90683.
| Key | What iOS prompts for | Typical features that require it |
|---|---|---|
| NSPhotoLibraryUsageDescription | Full read access to photos, videos, albums, and metadata | Image pickers, photo gallery views, OCR on user images, EXIF or location reads |
| NSPhotoLibraryAddUsageDescription | Write-only access to add a new asset to the camera roll | Saving a generated image, exporting a video file, archive-only flows |
The official rule from Apple is to declare the minimum scope. If the app only saves a screenshot the user just generated, the Add-only key is sufficient and the permission prompt shows a less alarming message. If the app uses UIImagePickerController to let the user pick an existing photo, the full read key is required, because the underlying API reads the library.
A build can declare both keys at the same time, which is the right pattern for apps that both read and write. Declaring only the Add key while the binary links against a read API will re-trigger ITMS-90683 on the next upload.
How do you add the purpose string in Xcode, Expo, React Native, or FlutterFlow?
The short answer is that every framework eventually writes to Info.plist; the variation is where you express the change in source.
Native Xcode project. Open Info.plist in the project navigator, add a new row, type NSPhotoLibraryUsageDescription as the key (Xcode auto-completes from a list), and put the purpose string in the value column. Rebuild and re-archive.
Expo (managed workflow). Add the key under the ios.infoPlist section of app.json or app.config.js:
{
"expo": {
"ios": {
"infoPlist": {
"NSPhotoLibraryUsageDescription": "Pick a photo from your library to attach to your post."
}
}
}
}
Expo's ImagePicker documentation recommends using the photosPermission field of the expo-image-picker config plugin instead, which writes the same key at prebuild time and keeps the message close to the dependency that needs it.
React Native (bare). Open ios/<AppName>/Info.plist and add the key with a string value, the same way you would in a native Xcode project. Pods can ship their own privacy manifests, but they cannot supply a top-level NSPhotoLibraryUsageDescription on behalf of the host app.
FlutterFlow. In the FlutterFlow editor, open App Settings, iOS, and add the entry under Custom Info.plist Entries. The exported Xcode project copies the value into Info.plist on the next build. Several FlutterFlow users have reported on the FlutterFlow issue tracker that early project templates did not include the key, so apps that used any image plugin failed ITMS-90683 on first submission.
Capacitor. Edit ios/App/App/Info.plist directly. The Capacitor Camera plugin documentation requires NSCameraUsageDescription, NSPhotoLibraryUsageDescription, and NSPhotoLibraryAddUsageDescription together for the standard photo-from-library flow.
Why does ITMS-90683 fire when your code does not touch the photo library?
The short answer is that one of your dependencies links against the Photo Library framework, even if your code never calls it. The validator scans the entire app bundle, including every static library and dynamic framework, and the symbol resolution does not care which Swift or Objective-C file imported it.
The common culprits are share extensions, analytics SDKs that capture screenshots, ad SDKs that read media metadata, social login SDKs that offer a set-profile-photo flow, and older versions of OCR or document-scanning libraries. Running nm or otool -L on the compiled framework binaries inside .app/Frameworks/ reveals which library imports the symbol:
nm -gU MyApp.app/Frameworks/*.framework/* 2>/dev/null | \
grep -E "PHAsset|PHPhotoLibrary|UIImagePickerController"
If a framework you did not author shows up, the right fix is either to add the purpose string (and accept that the user will see the prompt the first time the SDK triggers it) or to update the SDK to a version that hides the API behind a feature flag. According to the react-native-share issue thread on the same problem, older versions of the share library forced NSPhotoLibraryUsageDescription on every host app even when the share sheet was only used for plain text. Updating to a newer version that gates the photo path behind a runtime opt-in removed the requirement entirely.
What should the purpose string actually say?
The short answer is one honest sentence that names the feature, not a generic placeholder. Apple reviewers read these strings during human review, and a vague description can escalate the build to Guideline 5.1.1 even after the validator has passed.
A useful pattern is feature, reason, scope. "Pick a profile photo from your library" names the feature (profile photo), the reason (let the user choose), and the scope (read access to the library). The same pattern works for write keys: "Save the receipt as an image to your camera roll" names the feature (save receipt), the reason (offline copy), and the scope (write access).
Avoid the three patterns reviewers consistently flag: blank or boilerplate values (xxxxx, test), brand-only values (just the app name with no verb), and over-broad values (Allow MyApp to access your media, with no feature reference). The string also accepts $(PRODUCT_NAME) as a substitution variable, which Expo's documentation recommends so the app name stays consistent if it ever changes.
For builders who want an external automated read on a compiled build before it goes through App Review, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning of APK, AAB, and IPA files against OWASP MASVS controls and the App Store Connect validator family, including the missing purpose-string checks.
What to watch out for
The trap most teams hit twice is fixing ITMS-90683 by adding the photo library key, then failing on a different ITMS code at the next submission because the same SDK also referenced the camera, microphone, or location APIs. App Store Connect emits one ITMS code per missing key per upload, so the second build that adds only the photo string will fail again on a Missing purpose string in Info.plist - NSCameraUsageDescription line if the SDK touches the camera too. Audit every privacy API the bundle links against in one pass, not one error code at a time.
The other repeated pattern is the placeholder description that passes the validator but fails human review. A reviewer who reads "Allow access to photos" with no context can request more detail under Guideline 5.1.1, which adds at least one full review cycle. Two weeks lost for a single sentence.
A myth still in circulation says that setting the key to an empty string suppresses the prompt. It does not. The app crashes when the API is first called, because iOS treats an empty NSPhotoLibraryUsageDescription as undefined behavior. The string must contain real text for the runtime to display the alert.
Key takeaways
- Add
NSPhotoLibraryUsageDescriptionto Info.plist with a concrete one-sentence description that names the feature using the photos, then rebuild and resubmit through App Store Connect. - Audit every privacy API your SDKs link against in one pass with
nmorotool, not one ITMS code at a time, so the next submission does not fail on the next missing key. - Prefer
NSPhotoLibraryAddUsageDescriptionwhen the app only writes new assets, because the prompt is gentler and avoids over-requesting access from the user. - Use the feature, reason, scope pattern for the string value, and avoid placeholders that pass the validator but trip Guideline 5.1.1 in human review.
- For builders who want a second pair of eyes on the compiled binary before submission, PTKD.com (https://ptkd.com) is one of the platforms that runs OWASP MASVS-aligned static scans on APK, AAB, and IPA files and flags missing purpose strings against the App Store Connect rules.




