Why the iOS Bluetooth pairing dialog never appears

By · · 7 min read

The firmware team says the device requires bonding. You connect, you discover services, and iOS never shows a pairing alert. Nothing fails loudly enough to debug. Connecting to a peripheral does not start pairing on iOS, and there is no API that does. Pairing is a side effect of touching something the peripheral has chosen to protect.

Two words get used as though they mean the same thing, and cause 4 below only makes sense if you separate them. Pairing is the exchange that generates keys for the current connection. Bonding is storing those keys so the next connection can skip it. A device can pair without bonding. The failure most teams hit is a bonding failure that looks like pairing never starting.

1. Nothing you touched was protected

iOS begins pairing when the peripheral rejects an operation with an authentication or encryption error. The usual sequence is this: you read or write a characteristic, the peripheral answers with insufficient authentication, and iOS responds by starting the pairing exchange. Whether you see a prompt is a separate question, decided by the method the two sides negotiate, which is cause 3 below. A peripheral can also request security itself by sending an SMP Security Request, which starts the same exchange with no failed operation first, so a device that pairs the moment you connect is not contradicting any of this.

If no characteristic in the firmware is marked as requiring encryption, that rejection never happens and the alert never appears. This is a firmware-side permission setting, not an app-side one. On ESP-IDF it is the permission flags on the characteristic, such as the encrypted read and write permissions, and the exact constants moved between v4 and v5. On Nordic it is the security mode and level on the attribute metadata, which differ between the nRF5 SDK and nRF Connect SDK. An app cannot make a peripheral demand security it did not ask for.

2. You discovered the characteristic but never read it

Discovery does not itself require encryption. Walking the attribute table tells you a protected characteristic exists and triggers nothing, because you have not yet asked for its value. On a reconnect to an already bonded device the link is encrypted anyway, but that is the bond doing the work, not the discovery.

// Discovery alone is not enough. Reading the protected
// characteristic is what provokes the pairing exchange.
func peripheral(_ peripheral: CBPeripheral,
                didDiscoverCharacteristicsFor service: CBService,
                error: Error?) {
    guard let secured = service.characteristics?
        .first(where: { $0.uuid == protectedCharacteristicUUID }) else { return }

    peripheral.readValue(for: secured)
}

If you want pairing to happen at a predictable moment rather than whenever the user first reaches a protected feature, read a protected characteristic deliberately during setup. That turns an alert appearing in the middle of an unrelated screen into a step you control.

3. The peripheral uses Just Works, so there is nothing to show

Not every bond involves a dialog. If the firmware pairs with Just Works, meaning no passkey and no man-in-the-middle protection, iOS can complete the bond with no user-visible prompt. This behaviour has moved across iOS releases and has depended on whether the app was foregrounded, so treat it as something to verify on the version you ship against rather than a rule.

A prompt appears when the exchange needs something from the user, which in practice means a passkey to enter or a number to confirm. That depends on the input and output capabilities the peripheral declares. A device that declares no display and no keyboard cannot ask for a passkey, so it cannot produce that alert.

4. iOS still holds a bond the device has forgotten

This is the one I have watched cost teams the most time, because it appears late and only on hardware that has been through firmware development.

When a device is reflashed or its bond table is cleared, it loses the keys. iOS does not. On the next connection iOS offers keys the peripheral no longer recognises and the peripheral answers with an SMP failure meaning the key is missing. What iOS does next has varied across releases, but in my testing the common outcome is a disconnect shortly after connecting rather than a fresh pairing prompt, which sends people hunting for a connection bug that is not there.

  • The tell is a disconnect that happens immediately after connecting, only on units that were previously paired, and never on a phone that has not seen the device before.
  • Confirm it by testing with a second phone that has never paired with the device. If that one works, the bond state on the first phone is the problem.
  • An iOS app cannot delete a bond. There is no CoreBluetooth API for it, so this cannot be solved in code.

The usual advice is to send the user to Settings, Bluetooth, and Forget This Device. Be careful with that instruction, because a BLE accessory bonded through CoreBluetooth often does not appear in that list at all. Accessories tend to show up there when they also pair as a classic or HID device; a plain GATT peripheral your app bonded with may be invisible to the user, leaving them with nothing to forget and you with a support ticket. Check on real hardware before you write that sentence into your onboarding copy.

5. The prompt appears and pairing fails anyway

A dialog that shows and then fails is a different problem from one that never shows, and it usually means the two sides disagree about the method. A peripheral that asks for a passkey while declaring no way to display one is the common version. Note that when firmware demands man-in-the-middle protection and the declared input and output capabilities cannot provide it, the exchange normally fails during the pairing feature negotiation, before any prompt is drawn, so that case looks like cause 1 rather than this one.

Inspect the error rather than the alert. The ATT error tells you which stage rejected you:

func peripheral(_ peripheral: CBPeripheral,
                didUpdateValueFor characteristic: CBCharacteristic,
                error: Error?) {
    if let attError = error as? CBATTError {
        switch attError.code {
        case .insufficientAuthentication:
            // Expected before bonding. iOS should now begin pairing,
            // with or without a visible prompt.
            break
        case .insufficientEncryption:
            // Link is up but not encrypted to the level required.
            break
        default:
            break
        }
    }
}

One more reason the dialog you expected is not the dialog you get

Since iOS 18 Apple has offered AccessorySetupKit, which handles accessory discovery and pairing in its own system sheet and hands your app a peripheral that is already set up. If a project adopts it, the pairing experience stops looking like the CoreBluetooth flow described here, and the alert you were waiting for is replaced by Apple’s sheet. It is worth knowing which of the two paths your app is on before debugging a missing prompt, because they fail in different places.

The order worth checking them in

Test with a phone that has never seen the device. It separates a pairing bug from a stale bond in about a minute.

Start there, because a fresh phone rules out cause 4 immediately. Then confirm with firmware which characteristics require encryption and which pairing method the device requests, which settles causes 1 and 3 without any app changes. Only after those three should you go looking at your own code.

The primary sources here are Apple's CoreBluetooth and AccessorySetupKit documentation and the Security Manager chapter of the Bluetooth Core Specification, which is where the pairing methods and IO capability mapping are actually defined. Pairing behaviour changes across iOS versions and differs between peripheral stacks, so verify anything here against your own hardware and current firmware. If you have a device that pairs on Android and refuses on iOS, send over the GATT table with its permission flags and we can find the mismatch.

Related writing

All writing · Get in touch about a Bluetooth project