If App Store Connect rejected your upload with ITMS-90683 and a message about NSBluetoothAlwaysUsageDescription, the build was blocked before it ever reached TestFlight. The reason is a missing Info.plist key for a Bluetooth API that lives somewhere in your linked binary. The fix is one short XML entry; the harder part is figuring out which dependency pulled Core Bluetooth into the build.
Short answer
ITMS-90683 with a Bluetooth purpose string means your binary references Core Bluetooth, almost always through CBCentralManager or CBPeripheralManager, and Info.plist does not declare why. Add NSBluetoothAlwaysUsageDescription with a real human-readable reason, rebuild, and resubmit. According to Apple's documentation for NSBluetoothAlwaysUsageDescription, apps that target iOS 13 or later only need this single key for Core Bluetooth access, regardless of whether the binary acts as a central or a peripheral.
What you should know
- The check runs at upload time, not at runtime. App Store Connect inspects the linked binary for Core Bluetooth symbols, so the key is required whether your own code calls Bluetooth or not.
- One key covers iOS 13 and later. NSBluetoothAlwaysUsageDescription replaces the older NSBluetoothPeripheralUsageDescription as of iOS 13, and a modern build needs only the new key unless the deployment target still supports iOS 12 or earlier.
- Third-party SDKs are the most frequent cause. Cordova, Capacitor, Flutter plugins, MAUI bindings, and React Native modules have all triggered the error in apps whose authors never wrote a line of Bluetooth code.
- The string is user-facing. iOS reads your text aloud in VoiceOver and prints it on the Bluetooth privacy screen in Settings, so a placeholder string can still trip Guideline 5.1.1 at human review.
- Apple's key list is fixed. Inventing a name like NSBluetoothUsageDescription does not satisfy the check; only the published keys count.
What does the 'Missing Purpose String for Bluetooth' message actually mean?
The short answer is that ITMS-90683 is a build-time permission audit Apple runs against the linker output of your IPA. The scanner looks for symbols in your binary that map to APIs guarded by a permission prompt: Bluetooth, location, camera, microphone, contacts, photo library, calendar, motion, speech recognition, Face ID, and so on. Each guarded API has a fixed Info.plist key. If the symbol is present in the linked binary and the matching key is absent, the upload fails with ITMS-90683 and the name of the missing key.
For Bluetooth, the message reads roughly: "Missing purpose string in Info.plist. 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 NSBluetoothAlwaysUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data." The last sentence is the only reliable signal of which key to add.
According to Apple's guidance on protecting the user's privacy, every protected API has a fixed Info.plist key, and the linker-symbol scanner that runs inside App Store Connect surfaces that mapping at upload time. The audit does not care whether your own code calls CBCentralManager or whether the call path is reachable in normal use. If a third-party framework inside the binary links Core Bluetooth, the scanner flags it, and Xcode Organizer or xcrun altool returns the error in the upload log before TestFlight processing begins.
Which Info.plist key does iOS 13 and later require for Bluetooth?
For builds targeting iOS 13 and later, the answer is exactly one key: NSBluetoothAlwaysUsageDescription. Apple's reference page for NSBluetoothPeripheralUsageDescription marks the older key as deprecated and tells developers that the always-key now covers both central and peripheral roles. iOS shows the new string in the runtime permission dialog and on the Bluetooth privacy screen inside the Settings app.
If the deployment target still includes iOS 12 or earlier, both keys are needed. Older versions of iOS read NSBluetoothPeripheralUsageDescription when prompting for peripheral mode, while NSBluetoothAlwaysUsageDescription covers iOS 13 onward.
| Deployment target | Required Info.plist key(s) |
|---|---|
| iOS 13 and later | NSBluetoothAlwaysUsageDescription |
| iOS 12 and earlier still supported | NSBluetoothAlwaysUsageDescription plus NSBluetoothPeripheralUsageDescription |
| Background Bluetooth (any version) | Either of the above, plus the Background Modes capability with "Uses Bluetooth LE accessories" or "Acts as a Bluetooth LE accessory" enabled |
A few teams add a third key, NSBluetoothUsageDescription, after seeing it suggested on community posts. That key is not part of Apple's published Information Property List reference. The upload scanner ignores it, the runtime ignores it, and adding it does not satisfy ITMS-90683.
How do you find the SDK that pulled Core Bluetooth into your build?
The trap with this error is that searching your source tree for "CBCentralManager" usually returns nothing. The Core Bluetooth call lives inside a precompiled framework binary, often a static archive, a CocoaPod, or an XCFramework with no shipped source. Three commands cover most cases.
First, run grep -r "NSBluetooth" Pods/ (or node_modules/ for React Native, or your ios/Plugins/ folder for Cordova) to find any framework that documents its Bluetooth needs in its bundled example Info.plist. Mature SDKs ship that snippet alongside the package so integrators know which keys to add.
Second, run nm -gU on each .framework and each .a archive in the build artifact, then pipe through grep -i bluetooth or grep CBCentralManager. Symbols such as _OBJC_CLASS_$_CBCentralManager, _OBJC_CLASS_$_CBPeripheralManager, and any reference to CoreBluetooth.framework in the binary's load commands are what the App Store Connect scanner reacts to. If any of these appear in a third-party binary, your Info.plist needs NSBluetoothAlwaysUsageDescription, whether or not your own code calls Bluetooth.
Third, search the framework's release notes and issue tracker. The pattern is well documented: Cordova's iOS shell at one point linked CoreBluetooth.framework even without an active Bluetooth plugin, Flutter's older plugin set occasionally bundled a Bluetooth dependency through transitive packages, and several React Native push-notification SDKs touched CoreBluetooth for proximity features the developer did not realize were active. Pinning these dependencies to a version that ships a privacy manifest declaring the Bluetooth access often clears the warning at the next upload attempt.
What does an acceptable Bluetooth purpose string look like under Guideline 5.1.1?
Apple's App Review Guideline 5.1.1 on Data Collection and Storage requires the purpose string to clearly and completely describe the data use. In practice, that means three things: a complete sentence, a named feature that needs the data, and a named data type ("your nearby Bluetooth devices", "your fitness tracker", "your point-of-sale reader"). Placeholder strings like "Permission required" or copies of the key name fail human review even after the upload scanner accepts them.
A working example for a fitness app:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app connects to your heart-rate monitor and bike sensors so it can record live data during your workout. No Bluetooth data leaves the device.</string>
That string passes both checks. The upload scanner sees the key exists. The reviewer under Guideline 5.1.1 reads a sentence that names a feature in the binary and a real data type. No-code builders that auto-generate the string ("App needs Bluetooth access") often pass the upload check and then catch a 5.1.1 rejection at human review. Rewrite those strings before submission.
Why did the same build that passed last month suddenly fail?
Two reasons, both visible on the developer forums. First, Apple's symbol scanner updates without announcement. A symbol that was not on the watched list in 2023 can be flagged in 2026 once Apple adds it. Bluetooth is one of the categories where the watched list has grown, because CoreBluetooth and Core Location together carry proximity signals that Apple now treats with extra care. Second, dependency updates pull new code paths. A minor-version bump of a notification SDK or an analytics SDK can quietly introduce a Core Bluetooth call that did not exist in the previous version. Lock dependencies, diff the framework binaries between releases, and run nm against the artifact before each App Store Connect upload.
A third reason matters for no-code builders. FlutterFlow, Bubble, Thunkable, and Adalo ship their own iOS shells, and each release bundles a different set of native plugins. A build exported in May 2026 may include a beacon plugin that the same project did not include three months earlier, and ITMS-90683 fires only on the new build. Open the generated Runner/Info.plist (Flutter) or the equivalent file in the exported Xcode project and confirm which keys are declared before resubmitting.
What to watch out for
The first trap is adding every privacy key to be safe. App Review's human team flags purpose strings for APIs the app cannot reasonably use, because the description then becomes false. If a dependency is the only reason Bluetooth is in the binary, the string should describe the dependency feature ("Our point-of-sale module connects to your card reader over Bluetooth"), not invent a Bluetooth feature your app does not have.
The second trap is shipping a placeholder. The text is user-facing on the iOS permission prompt and inside Settings. Strings like "Test", "Required", or "App needs Bluetooth" are accepted by the upload scanner and routinely rejected by the human reviewer under Guideline 5.1.1. Rewrite them to one or two complete sentences naming the feature and the data type.
The third trap is targeting a deployment version that still includes iOS 12. Builds with iOS 12 still in the deployment target need NSBluetoothPeripheralUsageDescription as well as NSBluetoothAlwaysUsageDescription. Raise the deployment target to iOS 13 or later if there is no specific reason to keep the older OS. The OWASP MASVS privacy controls also recommend declaring only the minimum data set the app actually uses, not a superset added "just in case".
For builders who want an external automated read of their compiled IPA's Info.plist coverage and permission usage before they submit to App Store Connect, PTKD.com (https://ptkd.com) is one of the platforms focused specifically on pre-submission scanning aligned with OWASP MASVS for no-code and vibe-coded apps. The scanner flags missing purpose strings, missing privacy manifest entries, and SDK-induced API access that the developer may not realize the binary contains. A scan is faster than a TestFlight round-trip when the goal is to confirm Info.plist coverage before the next upload.
Key takeaways
- ITMS-90683 for Bluetooth is a symbol-based check. Adding NSBluetoothAlwaysUsageDescription, not removing code, is the fix in almost every case.
- One key, NSBluetoothAlwaysUsageDescription, covers iOS 13 and later. Add NSBluetoothPeripheralUsageDescription only when the deployment target still includes iOS 12 or earlier.
- The cause is usually a third-party SDK that links Core Bluetooth internally. Pin SDK versions, run
nmon framework binaries, and search release notes before guessing. - The user-facing string is reviewed twice. The upload scanner only checks the key exists; the human reviewer under Guideline 5.1.1 checks the sentence is honest about the feature and the data.
- Some teams scan compiled IPAs through external platforms like PTKD.com (https://ptkd.com) before each App Store Connect upload to catch missing permission descriptions, missing privacy manifest entries, and SDK-pulled APIs the developer did not realize are in the binary.




