ITMS-90683: Missing Purpose String in Info.plist
ITMS-90683 names the exact key you are missing. The fix is to add that key to your app target's Info.plist with a sentence explaining why your app needs the data, and upload again. You do not need to work out which permission it means; Apple already told you.
The message, in full:
"ITMS-90683: Missing Purpose String in Info.plist. Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a
NSCameraUsageDescriptionkey with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required."
Read the sentence starting "While your app might not use these APIs". Apple wrote that because it is the single most common complaint about this error, and the answer is that it does not matter.
Which key it names
Keys that have been reported in real ITMS-90683 messages:
NSCameraUsageDescriptionNSPhotoLibraryUsageDescriptionNSContactsUsageDescriptionNSLocationWhenInUseUsageDescriptionNSBluetoothAlwaysUsageDescriptionNSBluetoothPeripheralUsageDescription
Other purpose-string keys Apple documents, which the same check covers:
NSMicrophoneUsageDescription, NSPhotoLibraryAddUsageDescription, NSLocationAlwaysAndWhenInUseUsageDescription, NSCalendarsUsageDescription, NSRemindersUsageDescription, NSMotionUsageDescription, NSHealthShareUsageDescription, NSHealthUpdateUsageDescription, NSFaceIDUsageDescription, NSSpeechRecognitionUsageDescription, NSSiriUsageDescription, NSAppleMusicUsageDescription, NSHomeKitUsageDescription, NSLocalNetworkUsageDescription, NSNearbyInteractionUsageDescription.
NSUserTrackingUsageDescription is a purpose string too, but it belongs to App Tracking Transparency and its enforcement path differs. Do not assume ITMS-90683 is the code you would see for a missing tracking string.
Two Bluetooth keys exist because Apple split them. NSBluetoothPeripheralUsageDescription is the older key and NSBluetoothAlwaysUsageDescription is the current one. Apps supporting a range of iOS versions often need both, and the error will only name the one it checked.
Why you get it for something you do not use
Your app's binary contains every symbol from every dependency you link. The check reads the binary, not your source. So a Bluetooth string is demanded because something in your app references CoreBluetooth, even if the code path is unreachable and you have never opened the framework's documentation.
The dependencies that cause this most often:
- Permission wrapper libraries. These deliberately reference every permission API so they can offer a uniform interface. Flutter's
permission_handleris the best-known example, and it ships a documented way to compile out the permissions you do not use. If you are on Flutter and getting demands for three permissions you have never touched, start here. - Analytics and crash reporting SDKs, which sometimes read device characteristics through frameworks that require strings.
- Ad and attribution SDKs, which historically referenced a great deal.
- Camera or media SDKs pulled in for one small feature.
- Cordova and Capacitor plugins, where the plugin's own
plugin.xmlmay or may not add the keys for you.
Apple's suggested remedy for this case is in the error text: "You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs." In practice that is a long road, and adding the string is a five-minute one.
Where the key has to live
In the Info.plist of the app target. Not only in InfoPlist.strings.
This trips people who have already localised their app. InfoPlist.strings localises a purpose string; it does not declare one. If the key exists only in your .strings files, the check sees no key in Info.plist and fails, even though the string appears correctly at runtime in every language.
The working arrangement is both: the key in Info.plist with a default English string, and the same key in each InfoPlist.strings for the translations.
Two other placement mistakes:
- In an extension's plist but not the app's. The check runs against the app bundle. An extension having the key does not cover the app.
- In a plist that is not the one being built. Multiple build configurations with multiple plists is a reliable way to add a key to the file you are not shipping. Check the target's Build Settings for the actual
INFOPLIST_FILEpath.
Finding which SDK is responsible
If you want to remove the cause rather than declare a permission you do not use, find the framework first.
grep -rn "CBCentralManager\|CBPeripheralManager" Pods/ node_modules/ 2>/dev/null
For prebuilt binaries where a source search finds nothing, grep -rl reads binaries too and names the file:
grep -rl CBCentralManager Pods/ Carthage/ node_modules/ 2>/dev/null
Or against the built bundle, which covers the app binary and every embedded framework:
grep -rl CBCentralManager "MyApp.app"
Swap the class name for whichever framework the missing key belongs to: AVCaptureDevice for camera, CNContactStore for contacts, PHPhotoLibrary for photos, CLLocationManager for location.
Writing a string that survives review
Passing ITMS-90683 and passing App Review are two different bars, and the second one is stricter.
The upload check only asks whether a non-empty string exists. A reviewer asks whether it explains itself. A purpose string reading "Required" or "This app needs camera access" will clear the upload and can still earn you a rejection under the privacy guidelines, because it tells the user nothing.
What a good one does: name the feature and the benefit, in one sentence, in the user's language.
- Weak: "We need access to your photos."
- Better: "Choose a photo from your library to use as your profile picture."
- Weak: "Bluetooth access required."
- Better: "Connect to your heart rate monitor so workouts record accurately."
If a dependency forced you to declare a permission your app genuinely never uses, that is a real problem and the string is the wrong place to solve it. Either remove the dependency or find the compile-time flag that strips the permission. Writing a purpose string for a capability that does not exist in your app invites a reviewer to go looking for it.
Fix sequence
- Read the key name out of the error. That is the answer to "which permission".
- Add the key to the app target's
Info.plistwith a specific one-sentence string. - If you are localised, add the same key to every
InfoPlist.strings. - Bump the build number and upload again. The version string does not change.
- If you would rather not declare it, find the dependency with the greps above and remove or reconfigure it, then upload.
Step 4 is the whole cost of getting this wrong: one build number and one upload. It is the cheapest App Store error there is, which is worth remembering before you spend three hours hunting the SDK.