Create Free APK

JavaScript bridge API

Web Bluetooth

The navigator.bluetooth shape, answered natively so a page written for Chrome works unchanged.

navigator.bluetooth web standard#

navigator.bluetooth — Web Bluetooth

Example

Standard Web Bluetooth: the same navigator.bluetooth code you would write for Chrome works in the installed app, for any Bluetooth Low Energy device.

Returns: Promises, as in the web standard. Values are DataViews. Needs: Bluetooth. There is no switch: when you build from a ZIP or an HTML page, the build turns it on by itself if your code mentions navigator.bluetooth (or BluetoothUUID). A website (URL) build cannot turn it on. Without it, navigator.bluetooth does not exist. The first requestDevice() asks for the Nearby devices permission (Location on Android 11 and older).

Heart-rate strap, from a button tap. Check for navigator.bluetooth inside the handler - the app installs it while the page loads, not before your first script runs:

var hrDevice = null;

document.getElementById('connect').addEventListener('click', async function () {
  if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
  try {
    hrDevice = await navigator.bluetooth.requestDevice({
      filters: [{ services: ['heart_rate'] }],        // or { name: 'Polar H10' } / { namePrefix: 'Polar' }
      optionalServices: ['battery_service']
    });
    hrDevice.addEventListener('gattserverdisconnected', function () {
      showStatus('Strap disconnected');
    });
    var server = await hrDevice.gatt.connect();
    var hr = await server.getPrimaryService('heart_rate');
    var chr = await hr.getCharacteristic('heart_rate_measurement');
    chr.addEventListener('characteristicvaluechanged', function (e) {
      var v = e.target.value;                                   // DataView
      var bpm = (v.getUint8(0) & 1) ? v.getUint16(1, true) : v.getUint8(1);
      document.getElementById('bpm').textContent = bpm;
    });
    await chr.startNotifications();

    var battery = await server.getPrimaryService('battery_service');
    var level = await (await battery.getCharacteristic('battery_level')).readValue();
    document.getElementById('battery').textContent = level.getUint8(0) + '%';
  } catch (err) {
    // NotFoundError: user cancelled the chooser, Bluetooth is off, or no such service.
    // SecurityError: permission denied.  NetworkError: connection lost.
    showStatus(err.name + ': ' + err.message);
  }
});

document.getElementById('disconnect').addEventListener('click', function () {
  if (hrDevice && hrDevice.gatt.connected) hrDevice.gatt.disconnect();
});

Writing. writeValue and writeValueWithResponse wait for the device to confirm; writeValueWithoutResponse does not:

async function sendCommands(server) {
  // Heart Rate Control Point: 0x01 = reset energy expended
  var hr = await server.getPrimaryService('heart_rate');
  var ctrl = await hr.getCharacteristic('heart_rate_control_point');
  await ctrl.writeValueWithResponse(new Uint8Array([0x01]));

  // A custom UART-style service (full 128-bit UUIDs work everywhere)
  var uart = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');
  var rx = await uart.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e');
  await rx.writeValueWithoutResponse(new TextEncoder().encode('LED ON\n'));
}

Notes: The device chooser is drawn by the app and scans for 30 seconds; a page can only reach a device the user picked. A services filter matches only services the device advertises. After connect() every service the device has can be used, even ones not in optionalServices (keep them anyway, Chrome needs them). Also available: getAvailability(), getPrimaryServices(), getCharacteristics(), stopNotifications(), characteristic.properties, device.forget(), BluetoothUUID.getService/getCharacteristic/canonicalUUID. getDevices(), requestLEScan(), watchAdvertisements() and descriptors reject with NotSupportedError. For the standard fitness sensors with the numbers already parsed, see bleStartScan.

wbConnect bridge#

window.WebToApk.wbConnect(requestId: String, deviceId: String)

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind device.gatt.connect(). Pages use navigator.bluetooth; do not call this directly.

Returns: (through gatt.connect()) a Promise of the BluetoothRemoteGATTServer, after the device's services are discovered. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

async function connectAndList(device) {
  try {
    var server = await device.gatt.connect();
    var services = await server.getPrimaryServices();
    services.forEach(function (s) { console.log('service', s.uuid); });
    return server;
  } catch (err) {
    showStatus('Could not connect: ' + err.message);    // NetworkError, NotFoundError
    return null;
  }
}

Reconnect when the device drops. Calling connect() again on the same device works:

device.addEventListener('gattserverdisconnected', async function () {
  showStatus('Reconnecting...');
  try { await connectAndList(device); showStatus('Connected'); } catch (e) {}
});

Notes: Raw signature wbConnect(requestId, deviceId). Connecting to a device that is already connected resolves again at once. getPrimaryService(s) rejects with NetworkError before connect() has finished. Several devices can be connected at the same time.

wbDisconnect bridge#

window.WebToApk.wbDisconnect(deviceId: String)

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind device.gatt.disconnect() (and device.forget()). Pages use navigator.bluetooth; do not call this directly.

Returns: nothing (disconnect() is synchronous, as in the web standard). The device then fires gattserverdisconnected. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

var device = null;   // from navigator.bluetooth.requestDevice(...)

document.getElementById('stop').addEventListener('click', function () {
  if (device && device.gatt.connected) device.gatt.disconnect();
});

// Fires for your own disconnect() and when the device goes away by itself.
function watch(dev) {
  dev.addEventListener('gattserverdisconnected', function () {
    showStatus('Disconnected from ' + (dev.name || dev.id));
  });
}

Notes: Raw signature wbDisconnect(deviceId). After a disconnect the old service and characteristic objects stop receiving notifications; after the next connect(), get them again and call startNotifications() again. The app drops every connection when it closes.

wbGetCharacteristics bridge#

window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind service.getCharacteristic(uuid) and service.getCharacteristics(). Pages use navigator.bluetooth; do not call this directly.

Returns: (through the service) a Promise of one BluetoothRemoteGATTCharacteristic, or of an array of them, each with uuid and properties (read, write, writeWithoutResponse, notify, indicate, broadcast, authenticatedSignedWrites). Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

async function describe(server) {
  var service = await server.getPrimaryService('heart_rate');

  // One characteristic, by standard name, 16-bit number or full UUID:
  var measurement = await service.getCharacteristic('heart_rate_measurement'); // same as 0x2a37

  // All of them:
  var list = await service.getCharacteristics();
  list.forEach(function (c) {
    var p = c.properties;
    console.log(c.uuid, p.read ? 'read' : '', p.write ? 'write' : '', p.notify ? 'notify' : '');
  });
  return measurement;
}

Notes: Raw signature wbGetCharacteristics(requestId, deviceId, service). A UUID the service does not have rejects with NotFoundError; an unknown name throws TypeError (pass a full UUID for custom characteristics). The list is fetched once per service and then reused.

wbIsAvailable bridge#

window.WebToApk.wbIsAvailable(): Boolean

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind navigator.bluetooth.getAvailability(). Pages use navigator.bluetooth; do not call this directly.

Returns: (through getAvailability()) a Promise of true when the build has Bluetooth and the phone's Bluetooth is on, else false. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

async function updateBluetoothHint() {
  var hint = document.getElementById('bt-hint');
  if (!navigator.bluetooth) { hint.textContent = 'Bluetooth is not available in this app.'; return; }
  var on = await navigator.bluetooth.getAvailability();
  hint.textContent = on ? '' : 'Turn on Bluetooth to connect your sensor.';
}
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) updateBluetoothHint();
});

Notes: Raw signature wbIsAvailable(): Boolean (synchronous). It checks the adapter only - it does not ask for any permission. The availabilitychanged event of the web standard is not fired, so check again when the page becomes visible.

wbNotify bridge#

window.WebToApk.wbNotify(

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind characteristic.startNotifications() and stopNotifications(). Pages use navigator.bluetooth; do not call this directly.

Returns: (through the characteristic) a Promise of the characteristic itself. Each new value fires characteristicvaluechanged on it, with the value in event.target.value (a DataView). Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

var cadenceChr = null;

async function startCadence(server) {
  var svc = await server.getPrimaryService('running_speed_and_cadence');
  cadenceChr = await svc.getCharacteristic('rsc_measurement');
  cadenceChr.addEventListener('characteristicvaluechanged', onRsc);
  await cadenceChr.startNotifications();
}

function onRsc(e) {
  var v = e.target.value;
  var kmh = v.getUint16(1, true) / 256 * 3.6;
  var spm = v.getUint8(3);
  show('run', kmh.toFixed(1) + ' km/h, ' + spm + ' steps/min');
}

async function stopCadence() {
  if (!cadenceChr) return;
  cadenceChr.removeEventListener('characteristicvaluechanged', onRsc);
  try { await cadenceChr.stopNotifications(); } catch (e) {}
}

Notes: Raw signature wbNotify(requestId, deviceId, service, characteristic, enable). A characteristic that only supports indications is subscribed with indications automatically. A characteristic that cannot notify rejects with NotSupportedError. oncharacteristicvaluechanged = fn works too. After a disconnect, subscribe again.

wbRead bridge#

window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind characteristic.readValue(). Pages use navigator.bluetooth; do not call this directly.

Returns: (through readValue()) a Promise of a DataView; characteristic.value is updated too. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

async function readInfo(server) {
  try {
    var battery = await server.getPrimaryService('battery_service');
    var level = await (await battery.getCharacteristic('battery_level')).readValue();
    show('battery', level.getUint8(0) + '%');

    var info = await server.getPrimaryService('device_information');
    var maker = await (await info.getCharacteristic('manufacturer_name_string')).readValue();
    show('maker', new TextDecoder().decode(maker));
  } catch (err) {
    // NotFoundError: the device has no such service/characteristic
    // NotSupportedError: read rejected by the device   NetworkError: not connected / read failed
    show('info', err.message);
  }
}

Notes: Raw signature wbRead(requestId, deviceId, service, characteristic). Reads, writes and notification changes on one device run one after another in the order you call them, so you can fire several without waiting in between.

wbRequestDevice bridge#

window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind navigator.bluetooth.requestDevice(options): it shows the app's device chooser. Pages use navigator.bluetooth.requestDevice; do not call this directly.

Returns: (through requestDevice) a Promise of a BluetoothDevice {id, name, gatt}. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth) and the Nearby devices permission, asked on first use.

document.getElementById('pick').addEventListener('click', async function () {
  if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
  try {
    var device = await navigator.bluetooth.requestDevice({
      filters: [{ services: ['cycling_power'] }, { namePrefix: 'KICKR' }],
      optionalServices: ['battery_service']
    });
    showStatus('Picked ' + (device.name || device.id));
  } catch (err) {
    if (err.name === 'NotFoundError') showStatus('No device chosen.');   // also: Bluetooth off
    else showStatus(err.name + ': ' + err.message);                     // SecurityError = permission
  }
});

To list every nearby device instead of filtering: requestDevice({ acceptAllDevices: true }).

Notes: Raw signature wbRequestDevice(requestId, optionsJson); the answer comes back to the polyfill, not to the page. Call requestDevice from a tap. Filters use services (must be advertised), name (exact) and namePrefix; a filter matches when all its keys match, and the device matches when any filter matches. Neither filters nor acceptAllDevicesTypeError. Cancelling the chooser rejects with NotFoundError. The chooser scans for 30 seconds. device.id is the device's Bluetooth address.

wbWrite bridge#

window.WebToApk.wbWrite(

Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.

Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.

Example

Internal transport behind characteristic.writeValue(), writeValueWithResponse() and writeValueWithoutResponse(). Pages use navigator.bluetooth; do not call this directly.

Returns: (through the characteristic) a Promise of undefined. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).

With response (writeValue is the same): resolves when the device confirms the write.

async function setTargetPower(server, watts) {
  var ftms = await server.getPrimaryService('fitness_machine');
  var cp = await ftms.getCharacteristic('00002ad9-0000-1000-8000-00805f9b34fb'); // FTMS control point
  await cp.writeValueWithResponse(new Uint8Array([0x00]));                       // request control
  var cmd = new DataView(new ArrayBuffer(3));
  cmd.setUint8(0, 0x05);                 // set target power
  cmd.setInt16(1, watts, true);
  await cp.writeValueWithResponse(cmd);
}

Without response: faster, for streams where a lost packet does not matter.

async function sendText(characteristic, text) {
  if (!characteristic.properties.writeWithoutResponse) throw new Error('This characteristic needs writeValue');
  await characteristic.writeValueWithoutResponse(new TextEncoder().encode(text));
}

Notes: Raw signature wbWrite(requestId, deviceId, service, characteristic, valueBase64, withResponse). The value can be an ArrayBuffer, any typed array, a DataView or a plain array of bytes; anything else throws TypeError. Errors: NotSupportedError (write rejected), NetworkError (write failed or not connected), NotFoundError.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-09-23.