If Apple's automated submission scanner returned ITMS-90683 and a sentence about a missing Info.plist purpose string, the IPA never reached TestFlight. The upload log names a single privacy key, and a re-archive without that key in the right file will fail the same way. The rejection shows up most often in projects that were partly or entirely generated by Claude Code, Cursor, Windsurf, Replit Agent, FlutterFlow, or Bubble, because those tools write source files cleanly and skip bundle configuration.
Short answer
ITMS-90683 means a privacy-guarded symbol is linked into your iOS binary, and the matching NS*UsageDescription key is missing from the Info.plist that ships inside the IPA. Open the upload log, copy the key it names, add that key to Info.plist with a sentence that explains the feature in plain language, then re-archive and resubmit. According to Apple's guidance on protecting the user's privacy, every protected API has a fixed Info.plist key.
What you should know
- The check is symbol-based, not source-code-based. Even an SDK you forgot you installed can drag in a guarded API and trigger the error.
- The upload log names the exact key. Read the last sentence of the message; the missing key sits inside the quoted text.
- Apple expects the key in the base Info.plist. A value living only in a localized InfoPlist.strings file is treated as missing.
- A generic placeholder clears the upload scanner and can fail Guideline 5.1.1. Human reviewers reject strings that say 'App needs access' with no feature context.
- The valid key list is fixed. Inventing a name like NSPhotoUsageDescription does not satisfy the scan; only the published keys count.
What does ITMS-90683 actually mean when the upload log names a key?
The short answer is that ITMS-90683 is a build-time privacy audit Apple runs against the linker output of your IPA inside App Store Connect. The scanner walks the symbol table for references to APIs guarded by a runtime permission prompt: camera, microphone, location, photos, contacts, calendar, reminders, motion, health, Bluetooth, Face ID, speech recognition, Apple Music, and a handful of others. Each guarded API maps to one fixed Info.plist key. If the symbol is present and the matching key is absent, the upload fails before TestFlight processing begins.
The body of the message looks roughly like this: 'Your app's code references one or more APIs that access sensitive user data, or the app has one or more entitlements that permit such access. The Info.plist file should contain a [KEY] key with a user-facing purpose string explaining clearly and completely why your app needs the data.' The bracketed name is the only reliable signal of which permission the binary is asking for. A reproduction of this exact wording, with NSUserTrackingUsageDescription, sits in the Apple Developer Forums thread on ITMS-90683 for the App Tracking Transparency key.
The scanner does not care whether the call path is reachable, whether the symbol came from your own code, or whether the framework is dynamically loaded. If it is linked, the key is required.
Which Info.plist purpose-string keys does App Store Connect scan for?
The audit covers a published, fixed set of NS*UsageDescription keys. The table below groups the keys that account for almost every ITMS-90683 rejection seen in App Store Connect.
| Capability area | Required Info.plist key | Notes |
|---|---|---|
| Camera | NSCameraUsageDescription | Any camera frame access, including QR or document scan |
| Microphone | NSMicrophoneUsageDescription | Voice memo, video with audio, speech-to-text |
| Photos read | NSPhotoLibraryUsageDescription | Image pickers and gallery scans |
| Photos write only | NSPhotoLibraryAddOnlyUsageDescription | Save-to-camera-roll flows |
| Location in use | NSLocationWhenInUseUsageDescription | Required even for a single coarse fetch |
| Location always | NSLocationAlwaysAndWhenInUseUsageDescription | Required also for background location |
| Contacts | NSContactsUsageDescription | Any AddressBook or Contacts framework call |
| Calendar | NSCalendarsUsageDescription | EventKit references trigger this |
| Reminders | NSRemindersUsageDescription | EventKit reminders entitlement |
| Motion | NSMotionUsageDescription | Pedometer, accelerometer, gyro |
| Health read | NSHealthShareUsageDescription | HealthKit read access |
| Health write | NSHealthUpdateUsageDescription | HealthKit write access |
| Bluetooth | NSBluetoothAlwaysUsageDescription | iOS 13 and later, central or peripheral |
| Face ID | NSFaceIDUsageDescription | Local Authentication framework |
| Speech recognition | NSSpeechRecognitionUsageDescription | Speech framework |
| Tracking | NSUserTrackingUsageDescription | Required by App Tracking Transparency |
Apple's Information Property List reference for bundle resources holds the canonical list, including newer entries for nearby-interaction, focus status, and shared-with-you. If the upload log names a key that is not in the table above, search that reference page for the exact spelling.
Why do AI-coded and no-code app builds trigger ITMS-90683 so often?
Most AI coding agents and no-code exporters separate source generation from bundle configuration. Claude Code, Cursor, Windsurf, and Replit Agent write Swift, Dart, or TypeScript that compiles and runs in the simulator. The Info.plist file sits in a different mental category for them: it is XML inside the iOS project bundle, and the agents tend to edit it only when a tool call explicitly targets that file. Most generation prompts focus on the feature ('scan a barcode', 'show a map'), not the manifest entries that the feature implies at the linker level.
The same gap appears in no-code builders. FlutterFlow has a permission panel under project settings, but the FlutterFlow Help Center article on missing purpose strings confirms that several plugins drag in protected APIs without surfacing a UI control for the matching key. Bubble's iOS wrapper handles a fixed list of permissions and silently ignores anything an underlying plugin pulls in beyond that. OutSystems, Capacitor, and Cordova exhibit the same pattern, where a community plugin links a framework that the project's main config never mentions.
Two side effects make the gap worse in practice. First, the build succeeds locally because Info.plist is only required at upload, not at archive. Second, several agents and builders regenerate the iOS project from a higher-level config; a direct edit to Info.plist gets overwritten on the next regeneration if the source of truth is app.json, app.config.js, capacitor.config.ts, or the FlutterFlow project export.
Where do you put the purpose string in different build systems?
The location depends on the build pipeline that produced the IPA. Edit the file that ships inside the archive, not a sibling copy that lives one level up in the repository.
| Build system | File to edit | Notes |
|---|---|---|
| Native Xcode | Target/Info.plist | Property editor or raw XML |
| Expo managed | app.json or app.config.js (ios.infoPlist) | Run npx expo prebuild after editing |
| Bare React Native | ios/[App]/Info.plist | Add the key directly |
| FlutterFlow | Settings and Integrations, Permissions panel | Or edit Runner/Info.plist after export |
| Flutter (bare) | ios/Runner/Info.plist | Survives flutter clean |
| Capacitor | ios/App/App/Info.plist | Edit inside the Xcode project under App scheme |
| Cordova | config.xml edit-config block for Info.plist | The plugin then writes the key into Info.plist |
| .NET MAUI | Platforms/iOS/Info.plist | Plain XML, no agent regeneration |
After the edit, archive again in Xcode or run the equivalent command for your build pipeline, then upload through Transporter or Xcode Organizer. If the same key appears in the upload log a second time, you almost certainly edited the wrong file; many agent-generated projects keep two or more Info.plist files in different folders, and the linker only inspects the one Xcode bundles into the archive.
What if the key is in InfoPlist.strings but App Store Connect still rejects the build?
This is the most common follow-up case. A developer adds NSUserTrackingUsageDescription to en.lproj/InfoPlist.strings or another localized string file, archives, uploads, and sees the same ITMS-90683 message. The upload scanner reads the base Info.plist, not the localized fallbacks, so a key that lives only in InfoPlist.strings looks missing.
According to an Apple Developer Tools engineer in the Apple Developer Forums thread on InfoPlist.strings fallbacks, the key needs to exist in the base Info.plist as a fallback for any language the app does not localize. The localized InfoPlist.strings file is allowed to override the string at runtime, and several apps do that to translate the explanation, but it does not satisfy the upload audit on its own. The fix is to add the key to Info.plist with a development-language value, then keep the localized override in en.lproj/InfoPlist.strings or wherever the translated copy lives.
The same pattern applies if a build script strips Info.plist entries on release: the developer added the key locally, a CI step rewrote the file, and the audit caught the empty bundle. Inspect the Info.plist inside the archived IPA (right-click the IPA, 'Show Package Contents') to confirm the key reached the bundle before re-uploading.
What to watch out for
A generic purpose string like 'App needs access' clears the upload audit because the key exists, but App Review's human team often rejects that wording under Guideline 5.1.1. Replace it with one or two sentences naming the feature, the user benefit, and the data type. Avoid the trap of inventing a key name; only the published Information Property List keys count, and any other spelling is treated as missing. If an SDK you do not control is dragging in a protected API, document the SDK and the matching key in your README so future agent or CI runs do not strip the entry, because the audit runs again on every upload and remembers nothing about the last fix.
A second trap involves App Tracking Transparency. ATT requires NSUserTrackingUsageDescription whenever an SDK or any linked code references AppTrackingTransparency.framework, even if the call path stays gated behind a feature flag. Several analytics, attribution, and ad SDKs link that framework by default in their 2026 versions, and the upload scanner sees the symbol regardless of runtime behavior.
Key takeaways
- Read the last sentence of the ITMS-90683 message to find the exact key name; that single string is the only signal that matters for the fix.
- Add the key to the Info.plist that ships inside the archive, not the localized InfoPlist.strings, and not a sibling Info.plist that the build pipeline does not bundle.
- Use a real, feature-specific purpose string; a one-word value passes the audit and can still fail Guideline 5.1.1 with a human reviewer.
- Persist the edit inside the source of truth for your build pipeline (app.json, app.config.js, FlutterFlow project export, capacitor.config.ts) so the next regeneration does not erase it.
- For builders who want an automated read of the linked symbols and the Info.plist alignment before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning of compiled APK, AAB, and IPA bundles against OWASP MASVS and Apple's published privacy key list.




