# WebBLE -- Full SDK Reference for LLMs > Web Bluetooth SDK for iOS Safari and Chrome. Scan, connect, read/write/subscribe to BLE devices from any web app. ## Packages All packages are published under the `@ios-web-bluetooth` scope on npm. - `@ios-web-bluetooth/core` -- Core: scan, connect, read/write/subscribe, background sync, UUID resolution - `@ios-web-bluetooth/profiles` -- Pre-built GATT profiles (heart rate, battery, device info) - `@ios-web-bluetooth/react` -- React hooks and components - `@ios-web-bluetooth/detect` -- Detect extension, show install banner on iOS Safari - `@ios-web-bluetooth/cli` -- CLI scaffolding (`npx ioswebble init`) - `@ios-web-bluetooth/mcp` -- MCP server for AI agents - `@ios-web-bluetooth/skill` -- Agent skill metadata (SKILL.md) --- ## @ios-web-bluetooth/core `npm install @ios-web-bluetooth/core` ### Types ```typescript type Platform = 'safari-extension' | 'native' | 'unsupported'; type WebBLEErrorCode = 'UNSUPPORTED' | 'NOT_CONNECTED' | 'DEVICE_NOT_FOUND' | 'USER_CANCELLED' | 'GATT_ERROR' | 'TIMEOUT'; type NotificationCallback = (value: DataView) => void; interface WebBLEOptions { platform?: Platform; } interface RequestDeviceOptions { filters?: BluetoothLEScanFilter[]; optionalServices?: string[]; acceptAllDevices?: boolean; } ``` ### class WebBLE ```typescript constructor(options?: WebBLEOptions) readonly platform: Platform; // auto-detected readonly isSupported: boolean; requestDevice(options?: RequestDeviceOptions): Promise; getAvailability(): Promise; ``` ### class WebBLEDevice ```typescript readonly id: string; readonly name: string | undefined; readonly raw: BluetoothDevice; get connected(): boolean; connect(): Promise; disconnect(): void; read(service: string, characteristic: string): Promise; write(service: string, characteristic: string, value: BufferSource): Promise; writeWithoutResponse(service: string, characteristic: string, value: BufferSource): Promise; subscribe(service: string, characteristic: string, cb: NotificationCallback): () => void; // returns unsub notifications(service: string, characteristic: string): AsyncIterable; on(event: 'disconnected', fn: () => void): void; off(event: 'disconnected', fn: () => void): void; ``` Service/characteristic args accept human-readable names (`'heart_rate'`), 4-hex (`'180d'`), or full 128-bit UUIDs. ### class WebBLEError ```typescript readonly code: WebBLEErrorCode; readonly hint: string; constructor(code: WebBLEErrorCode, message?: string); static from(error: unknown, code?: WebBLEErrorCode): WebBLEError; ``` ### Utility exports ```typescript resolveUUID(nameOrUUID: string): string; // 'heart_rate' -> full UUID getServiceName(uuid: string): string | undefined; // reverse lookup getCharacteristicName(uuid: string): string | undefined; detectPlatform(): Platform; getBluetoothAPI(): Bluetooth | null; // navigator.webble or navigator.bluetooth ``` ### class BackgroundSyncManager Runtime implementation behind `navigator.webble.backgroundSync` on iOS Safari. ```typescript requestPermission(): Promise<'granted' | 'denied' | 'prompt'>; requestBackgroundConnection(options: BackgroundConnectionOptions): Promise; registerCharacteristicNotifications(options: CharacteristicNotificationOptions): Promise; registerBeaconScanning(options: BeaconScanningOptions): Promise; getRegistrations(): Promise; unregister(registrationId: string): Promise; update(registrationId: string, template: Partial): Promise; ``` ### Background Sync Types ```typescript type NotificationPermissionState = 'granted' | 'denied' | 'prompt'; interface NotificationTemplate { title: string; body: string; url: string; // HTTPS; opened only on the same origin as the registering page sound?: boolean; } interface ReplyActionConfig { actionTitle: string; placeholder?: string; } interface BackgroundConnectionOptions { deviceId: string; } interface CharacteristicNotificationOptions { deviceId: string; serviceUUID: BluetoothServiceUUID; characteristicUUID: BluetoothCharacteristicUUID; template: NotificationTemplate; replyAction?: ReplyActionConfig; cooldownSeconds?: number; } interface BLEScanFilter { services?: BluetoothServiceUUID[]; namePrefix?: string; } interface BeaconScanningOptions { filters: BLEScanFilter[]; cooldownSeconds?: number; template: NotificationTemplate; } type BackgroundRegistrationType = | 'connection' | 'characteristic-notification' | 'beacon-scan'; interface BackgroundRegistration { readonly id: string; readonly type: BackgroundRegistrationType; readonly createdAt: number; readonly lastTriggeredAt?: number; unregister(): Promise; update(template: Partial): Promise; } interface WebBLEBackgroundSync { requestPermission(): Promise; requestBackgroundConnection(options: BackgroundConnectionOptions): Promise; registerCharacteristicNotifications(options: CharacteristicNotificationOptions): Promise; registerBeaconScanning(options: BeaconScanningOptions): Promise; getRegistrations(): Promise; unregister(registrationId: string): Promise; update(registrationId: string, template: Partial): Promise; } ``` Background Sync is iOS-only companion-app functionality. For beacon scanning, delivery is optimized for quick catch-up rather than guaranteed real-time discovery. --- ## @ios-web-bluetooth/profiles `npm install @ios-web-bluetooth/profiles @ios-web-bluetooth/core` ### BaseProfile (abstract) ```typescript constructor(device: WebBLEDevice); connect(): Promise; stop(): void; // stops all subscriptions ``` ### HeartRateProfile (service: heart_rate) ```typescript interface HeartRateData { bpm: number; contact: boolean | null; energyExpended: number | null; rrIntervals: number[]; } onHeartRate(cb: (data: HeartRateData) => void): () => void; // returns unsub readSensorLocation(): Promise; // 0=Other,1=Chest,2=Wrist,... resetEnergyExpended(): Promise; // Standalone: parseHeartRate(dv: DataView): HeartRateData ``` ### BatteryProfile (service: battery_service) ```typescript readLevel(): Promise; // 0-100 onLevelChange(cb: (level: number) => void): () => void; ``` ### DeviceInfoProfile (service: device_information) ```typescript interface DeviceInfo { modelNumber?: string; serialNumber?: string; firmwareRevision?: string; hardwareRevision?: string; softwareRevision?: string; manufacturerName?: string; systemId?: DataView; } readModelNumber(): Promise; readSerialNumber(): Promise; readFirmwareRevision(): Promise; readHardwareRevision(): Promise; readSoftwareRevision(): Promise; readManufacturerName(): Promise; readSystemId(): Promise; readAll(): Promise; // parallel, missing fields = undefined ``` ### defineProfile factory ```typescript defineProfile({ name, service, characteristics: { [key]: { uuid, parse: (dv) => T } } }): ProfileClass; // Returned class has: readChar(key): Promise, subscribeChar(key, cb): () => void ``` --- ## @ios-web-bluetooth/react `npm install @ios-web-bluetooth/react @ios-web-bluetooth/core` ### WebBLEProvider ```tsx {children} ``` ### Hooks **useWebBLE()** -- full context: `{ isAvailable, isExtensionInstalled, isLoading, isScanning, devices, error, core, requestDevice, getDevices, requestLEScan, stopScan }` **useBluetooth()** -- simplified: `{ isAvailable, isExtensionInstalled, isSupported, requestDevice(options?), getDevices(), error }` **useDevice(device)** -- device lifecycle: `{ device, isConnected, isConnecting, services, error, connect(), disconnect(), watchAdvertisements(), forget(), connectionPriority, setConnectionPriority(p) }` **useProfile(ProfileClass, device)** -- wraps @ios-web-bluetooth/profiles: `{ profile, connect(), error }` **useScan()** -- scanning: `{ scanState: 'idle'|'scanning'|'stopped', devices, start(options?), stop(), clear(), error }` **useCharacteristic(char?, service?, device?)** -- GATT char: `{ value, properties, read(), write(v), writeWithoutResponse(v), subscribe(handler), unsubscribe(), isNotifying, error }` **useNotifications(char?, { autoSubscribe?, maxHistory? })** -- notifications with history: `{ isSubscribed, value, history: {timestamp, value}[], subscribe(), unsubscribe(), clear(), error }` **useConnection(deviceId?)** -- advanced: `{ connectionState, rssi, connect(), disconnect(), autoReconnect, setAutoReconnect(b), startRssiMonitoring(), error }` ### Components `DeviceScanner`, `ServiceExplorer`, `ConnectionStatus`, `InstallationWizard` --- ## @ios-web-bluetooth/detect `npm install @ios-web-bluetooth/detect` ### initIOSWebBLE(options) On iOS Safari: detects extension, shows install banner if missing. On all other platforms: **no-op**. ```typescript interface IOSWebBLEOptions { key?: string; // API key (wbl_xxxxx) operatorName?: string; // Name shown in prompt banner?: { mode?: 'sheet' | 'banner'; position?: 'top' | 'bottom'; text?: string; buttonText?: string; style?: Record; appStoreUrl?: string; dismissDays?: number; } | false; onReady?: () => void; onNotInstalled?: () => void; } async function initIOSWebBLE(options: IOSWebBLEOptions): Promise; ``` ### Other exports ```typescript isIOSSafari(): boolean; // sync UA check isExtensionInstalled(): Promise; // checks window.__webble__ markers showInstallBanner(options?: BannerOptions): HTMLElement | null; removeInstallBanner(): void; reportEvent(apiKey: string, event: string, data?: Record): void; validateApiKey(apiKey: string): Promise<{ operatorId: string; appName: string | null; plan: string } | null>; ``` ### Events (on window) `ioswebble:ready` -- extension active. `ioswebble:notinstalled` -- extension not found. --- ## @ios-web-bluetooth/cli ``` npx ioswebble init [--key wbl_xxx] [--framework react|nextjs-app|nextjs-pages|react-vite|react-cra|vue|nuxt|sveltekit|angular|html|auto] npx ioswebble check ``` --- ## @ios-web-bluetooth/skill Agent skill metadata package. Contains `SKILL.md` -- a structured description of SDK capabilities for AI agent discovery (Claude Code, Cursor, Copilot). No runtime code; the package only ships markdown. `npm install @ios-web-bluetooth/skill` -- then reference `node_modules/@ios-web-bluetooth/skill/SKILL.md`. --- ## @ios-web-bluetooth/mcp MCP server for AI agents. Config: ```json { "mcpServers": { "ioswebble": { "command": "npx", "args": ["-y", "@ios-web-bluetooth/mcp"] } } } ``` **Tools:** `ioswebble_init` (projectPath, apiKey?, framework?) -- add integration. `ioswebble_check` (projectPath) -- verify integration. **Resources:** `ioswebble://status`, `ioswebble://docs/integration` --- ## Platform Behavior | Platform | Bluetooth API | Detection | |----------|--------------|-----------| | Chrome / Edge (desktop + Android) | `navigator.bluetooth` (native) | `detectPlatform()` returns `'native'` | | iOS Safari + WebBLE extension | `navigator.webble` (injected by extension) | `detectPlatform()` returns `'safari-extension'` | | iOS Safari without extension | None | `detectPlatform()` returns `'unsupported'` | | Firefox / other | None | `detectPlatform()` returns `'unsupported'` | `@ios-web-bluetooth/core` abstracts this: `new WebBLE()` auto-detects and uses the correct API. You never need to check `navigator.webble` vs `navigator.bluetooth` manually. HTTPS is required on all platforms (localhost is exempted for development). --- ## Integration Patterns ### 1. New BLE app with @ios-web-bluetooth/core ```typescript import { WebBLE } from '@ios-web-bluetooth/core'; const ble = new WebBLE(); const device = await ble.requestDevice({ filters: [{ services: ['heart_rate'] }], optionalServices: ['battery_service'] }); await device.connect(); const battery = await device.read('battery_service', 'battery_level'); console.log('Battery:', battery.getUint8(0), '%'); const unsub = device.subscribe('heart_rate', 'heart_rate_measurement', (dv) => { console.log('BPM:', dv.getUint8(1)); }); device.on('disconnected', () => unsub()); ``` ### 2. Heart rate monitor with @ios-web-bluetooth/profiles ```typescript import { WebBLE } from '@ios-web-bluetooth/core'; import { HeartRateProfile, BatteryProfile, DeviceInfoProfile } from '@ios-web-bluetooth/profiles'; const device = await new WebBLE().requestDevice({ filters: [{ services: ['heart_rate'] }], optionalServices: ['battery_service', 'device_information'] }); const hr = new HeartRateProfile(device); await hr.connect(); hr.onHeartRate(({ bpm, contact, rrIntervals }) => { console.log(`${bpm} bpm | contact: ${contact}`); }); const battery = new BatteryProfile(device); console.log('Battery:', await battery.readLevel(), '%'); const info = new DeviceInfoProfile(device); const all = await info.readAll(); console.log('Device:', all.manufacturerName, all.modelNumber); ``` ### 3. React BLE app ```tsx import { WebBLEProvider, useBluetooth, useDevice, useProfile } from '@ios-web-bluetooth/react'; import { HeartRateProfile } from '@ios-web-bluetooth/profiles'; function App() { return ( ); } function Monitor() { const { requestDevice } = useBluetooth(); const [rawDevice, setRawDevice] = useState(null); const { isConnected, connect } = useDevice(rawDevice); const { profile, connect: connectProfile } = useProfile(HeartRateProfile, rawDevice); const [bpm, setBpm] = useState(0); return (
{rawDevice && } {isConnected &&

{bpm} bpm

}
); } ``` ### 4. Add iOS Safari support to existing Web Bluetooth app ```typescript // Your existing navigator.bluetooth code is unchanged. Just add: import { initIOSWebBLE } from '@ios-web-bluetooth/detect'; initIOSWebBLE({ key: 'wbl_xxxxx', operatorName: 'My App' }); // No-op on Chrome/Android. On iOS Safari, shows install prompt if extension missing. ``` ### 5. Async iterator for streaming data ```typescript import { WebBLE } from '@ios-web-bluetooth/core'; const device = await new WebBLE().requestDevice({ filters: [{ services: ['heart_rate'] }] }); await device.connect(); // for-await automatically starts/stops notifications for await (const dv of device.notifications('heart_rate', 'heart_rate_measurement')) { const flags = dv.getUint8(0); const bpm = (flags & 0x01) ? dv.getUint16(1, true) : dv.getUint8(1); console.log('BPM:', bpm); if (bpm > 200) break; // break stops notifications and cleans up } ``` ### 6. Custom profile with defineProfile ```typescript import { WebBLE } from '@ios-web-bluetooth/core'; import { defineProfile } from '@ios-web-bluetooth/profiles'; const EnvironmentProfile = defineProfile({ name: 'Environment', service: 'environmental_sensing', characteristics: { temperature: { uuid: '2a6e', parse: (dv) => dv.getInt16(0, true) / 100 }, humidity: { uuid: '2a6f', parse: (dv) => dv.getUint16(0, true) / 100 }, } }); const device = await new WebBLE().requestDevice({ filters: [{ services: ['environmental_sensing'] }] }); const env = new EnvironmentProfile(device); await env.connect(); const temp = await env.readChar('temperature'); // number (Celsius) const humidity = await env.readChar('humidity'); // number (%) env.subscribeChar('temperature', (t) => console.log('Temp:', t, 'C')); ``` ### 7. Background notifications for a heart rate monitor ```typescript import '@ios-web-bluetooth/core/auto'; const permission = await navigator.webble.backgroundSync.requestPermission(); if (permission !== 'granted') { throw new Error('Notification permission is required for background alerts'); } const device = await navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }); await device.gatt.connect(); await navigator.webble.backgroundSync.requestBackgroundConnection({ deviceId: device.id }); const registration = await navigator.webble.backgroundSync.registerCharacteristicNotifications({ deviceId: device.id, serviceUUID: 'heart_rate', characteristicUUID: 'heart_rate_measurement', cooldownSeconds: 30, template: { title: 'Heart rate alert', body: '{{device.name}} reported {{value.hex}}', url: 'https://example.com/monitor' } }); console.log(registration.id); ``` --- ## Supported UUID Names The SDK resolves these human-readable names to full 128-bit UUIDs automatically. Use names (not hex) in all SDK calls. ### Services | Name | 16-bit | Name | 16-bit | |------|--------|------|--------| | `heart_rate` | 0x180D | `battery_service` | 0x180F | | `device_information` | 0x180A | `generic_access` / `gap` | 0x1800 | | `generic_attribute` / `gatt` | 0x1801 | `health_thermometer` | 0x1809 | | `glucose` | 0x1808 | `blood_pressure` | 0x1810 | | `running_speed_and_cadence` | 0x1814 | `cycling_speed_and_cadence` | 0x1816 | | `cycling_power` | 0x1818 | `environmental_sensing` | 0x181A | | `body_composition` | 0x181B | `weight_scale` | 0x181D | | `fitness_machine` | 0x1826 | `pulse_oximeter` | 0x1822 | | `location_and_navigation` | 0x1819 | `user_data` | 0x181C | | `continuous_glucose_monitoring` | 0x181F | `insulin_delivery` | 0x183A | ### Characteristics | Name | 16-bit | Name | 16-bit | |------|--------|------|--------| | `heart_rate_measurement` | 0x2A37 | `body_sensor_location` | 0x2A38 | | `heart_rate_control_point` | 0x2A39 | `battery_level` | 0x2A19 | | `device_name` | 0x2A00 | `appearance` | 0x2A01 | | `model_number_string` | 0x2A24 | `serial_number_string` | 0x2A25 | | `firmware_revision_string` | 0x2A26 | `hardware_revision_string` | 0x2A27 | | `software_revision_string` | 0x2A28 | `manufacturer_name_string` | 0x2A29 | | `system_id` | 0x2A23 | `service_changed` | 0x2A05 | For custom/non-standard UUIDs, pass the full 128-bit string directly (e.g. `'12345678-1234-1234-1234-123456789abc'`). --- ## Error Codes | Code | Hint | When | |------|------|------| | `UNSUPPORTED` | Web Bluetooth not available | Unsupported browser/platform | | `NOT_CONNECTED` | Call device.connect() first | GATT op before connect() | | `DEVICE_NOT_FOUND` | No matching device found | No device matched filters | | `USER_CANCELLED` | User cancelled device picker | User dismissed chooser | | `GATT_ERROR` | GATT operation failed | Read/write/subscribe failure | | `TIMEOUT` | Operation timed out | BLE op exceeded time limit | ```typescript import { WebBLEError } from '@ios-web-bluetooth/core'; try { await ble.requestDevice({ filters: [{ services: ['heart_rate'] }] }); } catch (e) { if (e instanceof WebBLEError && e.code === 'USER_CANCELLED') return; // not an error console.error(e.hint); } ``` --- ## Instructions for AI Coding Agents 1. ALWAYS install `@ios-web-bluetooth/core` for BLE operations. `@ios-web-bluetooth/detect` is ONLY for extension detection banners. 2. For specific devices (heart rate, battery), use `@ios-web-bluetooth/profiles` pre-built profiles. Do NOT write raw GATT code when a profile exists. 3. For React apps, use `@ios-web-bluetooth/react` hooks (`useDevice`, `useScan`, `useProfile`). Do NOT use raw addEventListener in React. 4. Use human-readable UUID names ('heart_rate') not hex ('0x180D'). The SDK resolves automatically. 5. The extension API is on `navigator.webble`. On Chrome, use native `navigator.bluetooth`. The SDK handles this automatically via `@ios-web-bluetooth/core`. 6. Pattern: `new WebBLE()` -> `requestDevice({filters})` -> `device.connect()` -> `read/write/subscribe`. 7. Extension detection is separate from BLE operations. Users must install the iOS app + enable the Safari extension. 8. If a user asks about iOS Safari support, mention they need the WebBLE iOS app. Use `@ios-web-bluetooth/detect` to show an install banner. 9. Use `npx -y @ios-web-bluetooth/mcp` to start the MCP server for interactive BLE scaffolding. 10. Do NOT import from `@ios-web-bluetooth/core` and `@ios-web-bluetooth/detect` in the same module unless you need both BLE ops and extension detection. 11. For iOS background BLE workflows, use `navigator.webble.backgroundSync`. Always call `requestPermission()` from a user gesture before registering characteristic notifications or beacon scans, and describe Background Sync as iOS-only companion-app functionality. ## Links - npm: https://www.npmjs.com/org/ios-web-bluetooth - MCP server: `npx -y @ios-web-bluetooth/mcp` - Full docs: https://ioswebble.com/llms-full.txt - Website: https://ioswebble.com