Add Bluetooth to an app and the work is not the radio, it is one iOS key, three Android permissions and a real phone.
The answer first. To add Bluetooth to an app you declare one key on iOS, NSBluetoothAlwaysUsageDescription, and it covers scanning, connecting and advertising together. On Android targeting 12 or higher you declare three separate runtime permissions, BLUETOOTH_SCAN, BLUETOOTH_CONNECT and BLUETOOTH_ADVERTISE, and the user sees one prompt for Nearby devices. Android is the harder platform, and it got harder in API level 31 when it split scanning from connecting. Neither side can be finished in a simulator, because no simulator has a Bluetooth radio. Every key, permission and API name here was read from Apple's and Google's own documentation on 3 October 2026, and this page sits one level under the development guide.
Below: what you declare on each platform and what the old names were, the location permission Android used to force on anyone who scanned, what happens when someone taps Deny, and the day you lose if you plan this around an emulator.
See which prompts your app will triggerThe short version
iOS asks once for everything Android asks separately for each verb.
That one difference drives most of the work. Core Bluetooth treats the radio as a single thing you are allowed to use or not. On Android, looking for a device, talking to a device and being found by a device are three permissions with three reasons a user can refuse you, and until API level 31 the first was tangled up with location.
So write the Android path first and the iOS path is already done. Start on iOS and you meet the split halfway through, usually on the day you wire up pairing.
What you declare, and what the old names were
On iOS the whole declaration is one string in Info.plist. Apple says NSBluetoothAlwaysUsageDescription is required if your app uses the device's Bluetooth interface, and the Core Bluetooth reference is blunt about forgetting it: your app will crash if its Info.plist does not include usage description keys for the types of data it needs to access. Not a denied permission. A crash. If you are arriving from an older tutorial, the key you remember may be NSBluetoothPeripheralUsageDescription, which is the iOS 12 and earlier name, deprecated at iOS 13. Ship both only if your deployment target is earlier than iOS 13, because old devices read the peripheral key and new ones read the always key.
The API behind it is Core Bluetooth, which Apple describes as covering Bluetooth low energy and BR/EDR, the Classic radio. CBCentralManager is the central role, which scans and connects. CBPeripheralManager is the peripheral role, which advertises. All three verbs sit behind that single permission.
Android splits them. BLUETOOTH_SCAN is required to discover and pair nearby devices, BLUETOOTH_CONNECT to connect to a Bluetooth device the phone has already paired with, BLUETOOTH_ADVERTISE to advertise to nearby devices. All three arrived in API level 31 and all three are protection level dangerous, which is what makes them runtime requests rather than quiet manifest lines. The names they replaced are BLUETOOTH and BLUETOOTH_ADMIN, and you still declare those two with android:maxSdkVersion set to 30, so a device on Android 12 or higher is granted only the new set. The swap shows up in the API reference as well: BluetoothLeScanner.startScan wants BLUETOOTH_ADMIN from an app targeting Android 11 or lower and BLUETOOTH_SCAN from an app targeting Android 12 or higher.
Apple, Core Bluetooth framework reference, read 3 October 2026
Why an Android scan used to ask for your location
Before API level 31 there was no way to scan on Android without a location permission. Google gives the reason plainly: ACCESS_FINE_LOCATION is necessary because, on Android 11 and lower, a Bluetooth scan could potentially be used to gather information about the location of the user. A scan picks up the fixed radios around you, and that is a position fix whether you wanted one or not.
Target Android 12 or higher and it stops being automatic. You declare ACCESS_FINE_LOCATION only if your app uses scan results to derive physical location. If it does not, you can say so: add android:usesPermissionFlags set to neverForLocation on the BLUETOOTH_SCAN declaration, and set android:maxSdkVersion to 30 on ACCESS_FINE_LOCATION so older devices still work. Google names the cost in the same place, and it is real: include neverForLocation and some BLE beacons are filtered from your scan results. A beacon app cannot make the assertion and keep every beacon. If you do want position, that is closer to add maps location than to this page.
Two older cases survive. If your app supports a service and can run on Android 10 or Android 11, you must also declare ACCESS_BACKGROUND_LOCATION to discover devices. And since Android 8.0 there is a route around the whole problem: Google documents the Companion Device Manager as a more streamlined way to connect to companion devices, which provides a pairing UI on behalf of your app and does not require location permissions. If you pair with one accessory the user owns, read that before writing a scanner. How Android then polices a declared permission, and which ones Play asks you to justify, is a longer story that starts at the permissions this needs.
Google, Bluetooth permissions, Android developer documentation, read 3 October 2026
What the user is asked, and what a no costs you
On Android the three new permissions share one prompt. Google's wording: when your app requests at least one of them, the system prompts the user to allow your app to access Nearby devices. Good, because nobody is asked three times. Bad, because one refusal takes away scanning, connecting and advertising at once.
And you get two asks, ever. Google documents the rule: if the user taps Deny for a specific permission more than once during your app's lifetime of installation on a device, the system dialog no longer appears when your app asks again, because the action implies do not ask again and counts as a permanent denial. Nothing in your code undoes it, only Settings. Which is the argument for asking at the moment the user presses Connect rather than on first launch beside four other prompts. The same arithmetic applies to the other radios, so it reads the same way when you add nfc.
On iOS the refusal arrives as state, not as a callback result. CBManagerAuthorization has four cases: allowedAlways, denied, notDetermined and restricted. CBManagerState adds poweredOff, poweredOn, resetting, unauthorized, unknown and unsupported. Apple's rule for centralManagerDidUpdateState is to issue commands only when the state indicates poweredOn, and a state lower than poweredOn means scanning has stopped, which disconnects any previously connected peripherals. Treat denied and poweredOff as one screen. One asymmetry runs the other way, and it is the only place Android is kinder: send BluetoothAdapter.ACTION_REQUEST_ENABLE and you can ask the user to switch the radio on, with RESULT_OK or RESULT_CANCELED coming back. Core Bluetooth has nothing like it. The nearest thing is CBCentralManagerOptionShowPowerAlertKey, a Boolean that defaults to true and specifies whether the system warns the user if the app instantiates the central manager when Bluetooth service is not available. The system may warn. You cannot turn it on.
Check yours
What you actually have to declare
Tick what the app does. The lists are the permission sets on Apple's and Google's own pages, read 3 October 2026.
iOS, one key
NSBluetoothAlwaysUsageDescription in Info.plist, whatever you ticked. Leave it out and Core Bluetooth crashes the app rather than refusing it.
Android manifest
- BLUETOOTH_SCAN with usesPermissionFlags neverForLocation
- BLUETOOTH_CONNECT
- BLUETOOTH and BLUETOOTH_ADMIN, both with maxSdkVersion 30
On Android 12 and higher these are runtime requests, and the system prompts for Nearby devices. Deny it twice and the system stops showing that prompt at all.
You cannot finish this on a simulator
Plan for real devices from the first hour. Apple's current Core Bluetooth documentation does not describe Simulator support, and the one Apple document that addressed the question, Technical Note TN2295, is marked retired and describes an iOS 5 era rig with a USB adapter. Its warning is still the right instruction: test on a device with Bluetooth built in before submitting to App Review, and do not base a submission on the app running only in the simulator. On the Android side, the emulator release notes mention Bluetooth only as host audio bugs that were fixed. No emulated radio is documented anywhere. So this is one phone plus the accessory, or two phones if you are writing both halves.
Because that loop is slow, build the screens that need no radio first: the permission explanation, the denied state, the empty result, the scan with a timeout. Google's guidance is to stop as soon as you find the device you want, never scan on a loop, and always set a time limit, because scanning is battery intensive. Their sample uses ten seconds. Declare the hardware requirement while you are in the manifest: for a BLE app that is a uses-feature element naming android.hardware.bluetooth_le with required set to true, and Google is explicit that marking it required makes the Play store hide your app from devices lacking the feature. Set it to false for a bonus feature and check at runtime with PackageManager.hasSystemFeature. iOS has a matching key, UIRequiredDeviceCapabilities, with the value bluetooth-le for Bluetooth low energy hardware, but Apple's page does not state what the App Store does with it, so do not assume it filters installs the way Play does.
Two last things. Background work on iOS needs the Background Modes capability in Xcode, where the mode labelled Uses Bluetooth LE accessories is bluetooth-central and the one labelled Acts as a Bluetooth LE accessory is bluetooth-peripheral. And Google's BLE overview carries a caution that should shape your protocol before you write it: when a user pairs their device with another using BLE, the data communicated between the two devices is accessible to all apps on the user's device, so add app layer security if any of it is sensitive. Pairing is not encryption you can lean on.
The same four questions, answered per platform and target
| Platform and target | To look for devices | To connect to a paired device | Needs a location permission | Can ask the user to switch Bluetooth on |
|---|---|---|---|---|
| iOS 13 and later | NSBluetoothAlwaysUsageDescription | NSBluetoothAlwaysUsageDescription | not for Core Bluetooth | no API, system power alert only |
| iOS 12 and earlier | both usage keys, Always and Peripheral | both usage keys, Always and Peripheral | not for Core Bluetooth | no API, system power alert only |
| Android, target 12 or higher | BLUETOOTH_SCAN, runtime | BLUETOOTH_CONNECT, runtime | only to derive location from scan results | ACTION_REQUEST_ENABLE |
| Android, target 11 or lower | BLUETOOTH plus ACCESS_FINE_LOCATION | BLUETOOTH | Yes | ACTION_REQUEST_ENABLE |
| Android 10 or 11, discovery from a service | the above plus ACCESS_BACKGROUND_LOCATION | BLUETOOTH | Yes | ACTION_REQUEST_ENABLE |
Building the Bluetooth part of an app you own
The permission set is an afternoon. The state machine is the project. A Bluetooth screen has to survive a radio that is off, a permission that was permanently denied, a device that drifted out of range mid write, a connection the system dropped while the app was backgrounded, and a user who walked away with the scanner running. Write those five before the happy path.
React Native ships no Bluetooth API, so the radio arrives through a native module wrapping Core Bluetooth on one side and the android.bluetooth package on the other. None of the permission work above changes. Newly is an AI app builder: you describe the app in plain English and it writes a real React Native and TypeScript project you own, runs it on cloud iOS and Android simulators while it builds, and ships to TestFlight and Google Play internal testing. It is $25 a month and there is no free plan. The limit on this one feature is worth saying out loud: cloud simulators have no Bluetooth radio either, so the agent can write the module, the usage key and the manifest entries, but the pairing loop has to be tried on a phone. Take the code out as a ZIP from project Settings, or through the two way GitHub sync in Deploy, and run it on a device.
So sequence it the way the tooling allows. Get the permission flow, the prompt copy and every empty state reviewed on a simulator, where iteration is fast. Then take one build to a real phone and spend that session on the radio alone.
Questions people ask about Bluetooth in an app
On iOS, one Info.plist key: NSBluetoothAlwaysUsageDescription, which Apple says is required if your app uses the device's Bluetooth interface. It covers scanning, connecting and advertising. On Android targeting 12 or higher, three runtime permissions added in API level 31: BLUETOOTH_SCAN to discover and pair, BLUETOOTH_CONNECT to talk to an already paired device, BLUETOOTH_ADVERTISE to be discoverable. You also keep legacy BLUETOOTH and BLUETOOTH_ADMIN declarations with android:maxSdkVersion set to 30. Read from Apple's and Google's documentation on 3 October 2026.
Write down the five states before the first scan
Radio off, permission permanently denied, nothing found, connection dropped mid write, connected. Build those five screens, put the usage key and the manifest entries in, then take one build to a real phone and spend the session on the radio.
Start building