Skip to main content

Overview

The Whoop 4.0 can broadcast real-time heart rate data over standard BLE Heart Rate Service (UUID 0x180D). This allows third-party apps and devices to read heart rate without using the Whoop app.

Broadcast Toggle Commands

Broadcast is controlled by sending toggle commands to CMD_TO_STRAP:
aa0800a823 07 0e 00 c7e40f08  # Off
aa0800a823 08 0e 01 6c935474  # On
aa0800a823 09 0e 00 cdc99102  # Off

Packet Structure

OffsetSizeFieldDescription
0x005 bytesHeaderAlways aa0800a823
0x051 bytePacket CountIncrements with each packet
0x061 byteCategory0x0e for heart rate broadcast
0x071 byteState0x00 = Off, 0x01 = On
0x084 bytesChecksumCRC-32 with custom parameters
The packet count field is not validated by the device. You can use any value.

Enabling Broadcast

Using gatttool

# Turn ON heart rate broadcast
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX \
  --char-write -a 0x0010 -n aa0800a823080e016c935474

# Turn OFF heart rate broadcast
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX \
  --char-write -a 0x0010 -n aa0800a823070e00c7e40f08

Using Python (bleak)

After enabling broadcast, you can read heart rate using the standard BLE Heart Rate Service:
import asyncio
from bleak import BleakClient, BleakScanner

HEART_RATE_SERVICE = "00002a37-0000-1000-8000-00805f9b34fb"

async def read_heart_rate(address):
    device = await BleakScanner.find_device_by_address(address)
    if not device:
        print("Device not found")
        return
    
    async with BleakClient(device) as client:
        def callback(sender, data):
            # First byte contains flags
            # Second byte is heart rate value
            hr = int(data[1])
            print(f"Heart Rate: {hr} bpm")
        
        await client.start_notify(HEART_RATE_SERVICE, callback)
        await asyncio.sleep(60)  # Read for 60 seconds
        await client.stop_notify(HEART_RATE_SERVICE)

# Run with your device MAC address
asyncio.run(read_heart_rate("XX:XX:XX:XX:XX:XX"))

Reading Heart Rate

Once broadcast is enabled, the device advertises on the standard Heart Rate Service:
  • Service UUID: 0x180D (Heart Rate)
  • Characteristic UUID: 00002a37-0000-1000-8000-00805f9b34fb (Heart Rate Measurement)
  • Update Rate: ~1 second

Heart Rate Measurement Format

The heart rate characteristic follows the Bluetooth SIG specification:
ByteFieldDescription
0FlagsFormat and sensor contact status
1HR ValueHeart rate in beats per minute (BPM)

Device Visibility

The Whoop 4.0 only appears in BLE scans when heart rate broadcast is enabled. When broadcast is off, the device cannot be discovered by third-party apps.

Testing Broadcast State

# Try to find the device (will fail if broadcast is off)
python3 enable_notifications.py --address XX:XX:XX:XX:XX:XX \
  00002a37-0000-1000-8000-00805f9b34fb
If you get “ERROR: could not find device”, broadcast is disabled.

App Behavior

The Whoop app cannot read the broadcast state from the device. If broadcast is enabled in the app, it periodically sends 0x01 commands to ensure it stays on.

Characteristic Details

CMD_TO_STRAP (Control)

  • UUID: 61080002-8d6d-82b8-614a-1c8cb0f8dcc6
  • Handle: 0x0010
  • Properties: Write only

Heart Rate Measurement (Read)

  • UUID: 00002a37-0000-1000-8000-00805f9b34fb
  • Properties: Notify

Use Cases

  • Third-party fitness apps: Connect Whoop to Strava, Zwift, etc.
  • External displays: Show heart rate on bike computers or smartwatches
  • Custom monitoring: Build your own heart rate logging system
  • Integration: Use Whoop as a heart rate sensor for any app that supports standard BLE HR

Build docs developers (and LLMs) love