# Web Bluetooth API

> navigator.bluetooth.requestDevice() opens a chooser for one Bluetooth Low Energy device and exposes its GATT services. Filters, every exception, examples.

`navigator.bluetooth.requestDevice()` shows a browser-owned chooser listing nearby Bluetooth Low Energy devices that match the filters you pass, and resolves with the one `BluetoothDevice` the user picks. From that object a page connects to the device's GATT server and reads, writes, or subscribes to characteristics, so a PWA can talk to a heart-rate strap, a printer, or a lamp without a native companion app.

The API ships in Chromium only. Chrome 56 supports it on Android, ChromeOS, and macOS, Chrome 70 added Windows, and Linux still needs `chrome://flags/#enable-experimental-web-platform-features` ([implementation status](https://github.com/whatwg/bluetooth/blob/main/implementation-status.md), github.com); Edge 79, Samsung Internet 6.0, and Opera 43 follow Chrome. Firefox has an open tracking bug ([674737](https://bugzilla.mozilla.org/show_bug.cgi?id=674737)), WebKit records a formal "oppose" position ([standards-positions #570](https://github.com/WebKit/standards-positions/issues/570)), and Android WebView does not expose `navigator.bluetooth`.

## Syntax

```js
navigator.bluetooth.requestDevice(options)
navigator.bluetooth.getDevices()
navigator.bluetooth.getAvailability()

device.gatt.connect()
server.getPrimaryService(service)
service.getCharacteristic(characteristic)
characteristic.readValue()
characteristic.writeValueWithResponse(value)
characteristic.startNotifications()
```

`requestDevice()` returns a `Promise<BluetoothDevice>`; `getDevices()` returns a `Promise<sequence<BluetoothDevice>>` of devices this origin was previously granted, without a prompt, and is behind `chrome://flags/#enable-web-bluetooth-new-permissions-backend` from Chrome 85 (BCD `api.Bluetooth.getDevices`). `getAvailability()` resolves to `true` when an adapter is present. The `Bluetooth` interface is `[Exposed=Window, SecureContext]`: it is not available in workers and `navigator.bluetooth` is `undefined` on insecure origins.

## Parameters

`requestDevice()` takes one `RequestDeviceOptions` dictionary. Exactly one of `filters` or `acceptAllDevices: true` must be given; the chooser only lists devices that match at least one filter and no exclusion filter.

| Member | Type | Required | Description |
|---|---|---|---|
| `filters` | `sequence<BluetoothLEScanFilterInit>` | One of `filters` / `acceptAllDevices` | Devices matching any filter are shown. Must be non-empty when present. |
| `exclusionFilters` | `sequence<BluetoothLEScanFilterInit>` | No | Devices matching any of these are hidden even if they match `filters`; requires `filters`. Chrome 114. |
| `optionalServices` | `sequence<BluetoothServiceUUID>` | No, default `[]` | Services the page may access after connecting that are not already named in a `services` filter. A service missing from both is unreachable with `SecurityError`. |
| `optionalManufacturerData` | `sequence<unsigned short>` | No, default `[]` | Company identifiers whose manufacturer data the page may read from advertisements. |
| `acceptAllDevices` | `boolean` | One of `filters` / `acceptAllDevices` | List every nearby device instead of filtering. |

Each `BluetoothLEScanFilterInit` needs at least one member.

| Member | Type | Description |
|---|---|---|
| `services` | `sequence<BluetoothServiceUUID>` | Advertised services; a 16-bit alias (`0x180f`), a GATT name (`'battery_service'`), or a 128-bit UUID string. Must be non-empty. |
| `name` | `DOMString` | Exact device name, at most 248 UTF-8 bytes. |
| `namePrefix` | `DOMString` | Name prefix, non-empty and at most 248 UTF-8 bytes. |
| `manufacturerData` | `sequence<BluetoothManufacturerDataFilterInit>` | `{ companyIdentifier, dataPrefix?, mask? }`; identifiers must be unique within one filter. Chrome 92. |
| `serviceData` | `sequence<BluetoothServiceDataFilterInit>` | `{ service, dataPrefix?, mask? }`. |

`getPrimaryService()`, `getCharacteristic()`, and their plural forms take a `BluetoothServiceUUID` or `BluetoothCharacteristicUUID` in the same three spellings.

## Exceptions

`requestDevice()` rejects with one of the following; the `TypeError` checks run first, before any radio activity.

| Exception | Condition |
|---|---|
| `TypeError` | `exclusionFilters` without `filters`; both or neither of `filters` and `acceptAllDevices: true`; an empty `filters` or `exclusionFilters` array; a filter with no members; an empty `services`, `manufacturerData`, or `serviceData` array; a `name` or `namePrefix` over 248 UTF-8 bytes or an empty `namePrefix`; duplicate `companyIdentifier` values; an empty `dataPrefix`; a `mask` whose length differs from `dataPrefix`. |
| `SecurityError` | The document is not allowed to use the `bluetooth` Permissions Policy feature; the call has no transient user activation; or a filter or `optionalServices` entry names a blocklisted service UUID. |
| `NotFoundError` | The chooser closed with no device selected, or no adapter or matching device exists. |

The GATT methods reject differently. `device.gatt.connect()` rejects with `NetworkError` when the device is out of range or the link fails. `getPrimaryService()` and `getCharacteristic()` reject with `SecurityError` for a service not granted through `filters` or `optionalServices` or a blocklisted UUID, with `NetworkError` when `gatt.connected` is `false`, and with `NotFoundError` when the device lacks the requested item. `readValue()` and `writeValueWithResponse()` reject with `SecurityError` for UUIDs blocklisted for reads or writes, `InvalidModificationError` for a value over 512 bytes, `InvalidStateError` when the service or characteristic no longer exists, `NotSupportedError` when the device refuses the operation, and `NetworkError` on disconnect mid-operation. All of these are listed in the specification's per-method steps linked from See also.

:::observed
Calling `navigator.bluetooth.requestDevice()` outside a user-activation handler in Chrome rejects with `SecurityError: Must be handling a user gesture to show a permission request.`; closing the chooser without choosing rejects with `NotFoundError: User cancelled the requestDevice() chooser.`; passing neither `filters` nor `acceptAllDevices` throws `TypeError: Failed to execute 'requestDevice' on 'Bluetooth': Either 'filters' should be present or 'acceptAllDevices' should be true, but not both.`; a device that walks out of range during `gatt.connect()` rejects with `NetworkError: Bluetooth Device is no longer in range.`. The strings are defined in Chromium's [`bluetooth.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/bluetooth/bluetooth.cc) and [`bluetooth_error.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc) (chromium.googlesource.com).
:::

## Examples

Every example feature-detects `navigator.bluetooth` and shows what the page does without it; the first two must run from a `click` or similar handler because of the activation rule.

### Reading a battery level with a fallback message

Filter on the standard Battery Service so the chooser shows only devices that advertise it, connect, and read the single-byte Battery Level characteristic. When the API is missing the function returns `null` and the UI explains that the browser cannot reach Bluetooth devices.

```js
async function readBatteryLevel() {
  if (!('bluetooth' in navigator)) return null;

  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ['battery_service'] }],
  });
  const server = await device.gatt.connect();
  const service = await server.getPrimaryService('battery_service');
  const characteristic = await service.getCharacteristic('battery_level');
  const value = await characteristic.readValue();
  return value.getUint8(0);
}

document.querySelector('#battery').addEventListener('click', async () => {
  const output = document.querySelector('#battery-output');
  try {
    const level = await readBatteryLevel();
    output.textContent = level === null
      ? 'This browser cannot access Bluetooth devices; check the device itself.'
      : `Battery: ${level}%`;
  } catch (err) {
    if (err.name === 'NotFoundError') return; // chooser closed
    output.textContent = `${err.name}: ${err.message}`;
  }
});
```

`'battery_service'` and `'battery_level'` are GATT assigned names the browser maps to `0x180F` and `0x2A19`; a 128-bit UUID string works the same way for vendor services.

### Subscribing to heart-rate notifications and surviving a disconnect

Notifications arrive as `characteristicvaluechanged` events after `startNotifications()`. The device may walk out of range at any time, so listen for `gattserverdisconnected` on the device and offer a reconnect that calls `gatt.connect()` again on the same object, which needs no new chooser.

```js
async function watchHeartRate(onBeat, onLost) {
  if (!('bluetooth' in navigator)) {
    onLost('Bluetooth is not available in this browser');
    return;
  }

  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ['heart_rate'] }],
  });
  device.addEventListener('gattserverdisconnected', () => onLost('Device disconnected'));

  const server = await device.gatt.connect();
  const service = await server.getPrimaryService('heart_rate');
  const measurement = await service.getCharacteristic('heart_rate_measurement');

  measurement.addEventListener('characteristicvaluechanged', (event) => {
    const data = event.target.value;
    const is16Bit = data.getUint8(0) & 0x1;
    onBeat(is16Bit ? data.getUint16(1, true) : data.getUint8(1));
  });
  await measurement.startNotifications();
}
```

The first byte of a Heart Rate Measurement is a flags field; bit 0 says whether the rate that follows is 8 or 16 bits wide, which is why the handler branches before reading.

### Checking for an adapter before showing a pairing button

`getAvailability()` tells a page whether the machine has a Bluetooth radio at all, so it can hide a "Connect" button on a desktop without one instead of letting the user hit `NotFoundError`. It does not prompt and needs no activation.

```js
async function bluetoothUiState() {
  if (!('bluetooth' in navigator)) return 'unsupported';
  const available = await navigator.bluetooth.getAvailability();
  return available ? 'ready' : 'no-adapter';
}

const state = await bluetoothUiState();
document.querySelector('#connect').hidden = state !== 'ready';
document.querySelector('#bluetooth-note').textContent = {
  unsupported: 'Use Chrome or Edge on a desktop or Android device to connect over Bluetooth.',
  'no-adapter': 'No Bluetooth adapter was found on this device.',
  ready: '',
}[state];
```

Availability can change while the page is open (a USB dongle plugged in, Bluetooth toggled off); the `availabilitychanged` event on `navigator.bluetooth` reports the new value.

## See also

- [Web Serial API](/reference/capabilities/web-serial/), the same chooser model for serial ports
- [WebUSB API](/reference/capabilities/web-usb/)
- [WebHID API](/reference/capabilities/web-hid/)
- [Web Bluetooth: requestDevice() method](https://bluetooth.spec.whatwg.org/#dom-bluetooth-requestdevice) (bluetooth.spec.whatwg.org)
- [Web Bluetooth: getDevices() method](https://bluetooth.spec.whatwg.org/#dom-bluetooth-getdevices) (bluetooth.spec.whatwg.org)
- [WebKit standards position: Web Bluetooth](https://github.com/WebKit/standards-positions/issues/570) (github.com)
- [Mozilla bug 674737: Implement Web Bluetooth](https://bugzilla.mozilla.org/show_bug.cgi?id=674737) (bugzilla.mozilla.org)
- [Communicating with Bluetooth devices over JavaScript](https://developer.chrome.com/docs/capabilities/bluetooth) (developer.chrome.com)