A MagicMirror² module that displays your WHOOP health data – recovery score, sleep performance, and day strain – with deltas to the previous day and optional activity details. Supports multiple users on one mirror.
- Three headline scores – Recovery, Sleep Performance, and Day Strain
- Previous day deltas – see at a glance if you're trending up or down
- Color-coded zones – green/yellow/red for Recovery and Sleep; neutral white for Strain
- Today's activities – optional display of workouts with strain, distance, and calories
- Multi-user support – multiple WHOOP accounts on one mirror
- Automatic token refresh – authenticate once per user, runs indefinitely
- Serialized scheduling – no concurrent API calls or token refresh races
- Crash-safe token storage – token writes are atomic, so a power cut can't corrupt your credentials
- Stale-data mode – keeps showing last good data on API errors
- A WHOOP device with an active membership (per user)
- A WHOOP Developer account and registered app
cd ~/MagicMirror/modules
git clone https://github.com/frankrenehan/MMM-Whoop.git
cd MMM-Whoop
npm install- Go to developer-dashboard.whoop.com
- Sign in with your WHOOP account
- Create a new app (you'll be prompted to create a Team first)
- Set the Redirect URI to
http://localhost:3456/callback - Select the scopes the module needs:
read:recovery,read:cycles,read:sleep,read:workout - Note your Client ID and Client Secret
Note: You do not need
read:profileorread:body_measurement– the module does not use them. WHOOP recommends requesting only the scopes your app actually uses.
Run the setup script from any machine with a browser:
node setup.js --user-id alice --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRETThis saves tokens to whoop_tokens_alice.json in the module directory.
For a second user, have them log in to their WHOOP account and run:
node setup.js --user-id bob --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRETTip: You can run this on your laptop and
scpthe token file to the Pi:scp whoop_tokens_alice.json pi@YOUR_PI_IP:~/MagicMirror/modules/MMM-Whoop/
Single user:
{
module: "MMM-Whoop",
position: "bottom_left",
config: {
userId: "alice",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
}
},Two users:
{
module: "MMM-Whoop",
position: "bottom_left",
config: {
userId: "alice",
displayName: "ALICE",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
showActivities: true,
}
},
{
module: "MMM-Whoop",
position: "bottom_left",
config: {
userId: "bob",
displayName: "BOB",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
showActivities: false,
}
},Each user instance operates independently – its own tokens, its own polling timer, its own data.
cd ~/MagicMirror
npm start| Option | Type | Default | Description |
|---|---|---|---|
userId |
string |
"default" |
Unique identifier for this user (e.g. "alice", "bob") |
displayName |
string |
"" |
Label shown above scores (e.g. "ALICE"). Hidden when empty. |
clientId |
string |
"" |
Your WHOOP app Client ID |
clientSecret |
string |
"" |
Your WHOOP app Client Secret |
updateInterval |
int |
900000 |
Update interval in ms (15 min) |
showActivities |
bool |
true |
Show today's workouts below scores |
maxActivities |
int |
3 |
Maximum number of activities to display |
useEmoji |
bool |
false |
Use emoji sport icons instead of bullet points (requires emoji font) |
retryDelay |
int |
30000 |
Base retry delay on failure in ms (exponential backoff) |
animationSpeed |
int |
1000 |
DOM update animation speed in ms |
tokenFile |
string |
whoop_tokens_<userId>.json |
Token filename inside the module directory. Must be a plain filename (no path separators). |
tokenPath |
string |
"" |
Optional absolute path to this user's token file (e.g. "/home/pi/.config/mmm-whoop/alice.json"). Takes precedence over tokenFile. A relative path is rejected and that user's instance will not start. |
By default tokens live in the module directory as whoop_tokens_<userId>.json. To keep them elsewhere, point tokenPath at the location you want:
{
module: "MMM-Whoop",
position: "bottom_left",
config: {
userId: "alice",
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
tokenPath: "/home/pi/.config/mmm-whoop/alice.json",
}
},The module reads and refreshes tokens at that path. tokenPath must be absolute; when it is set, tokenFile is ignored.
Give setup.js the same path so that authorizing writes the token file where the module will look for it:
node setup.js --user-id alice --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET \
--token-path /home/pi/.config/mmm-whoop/alice.jsonThe directory must already exist and be writable; setup.js checks this before opening the browser, since an authorization code can only be exchanged once. If you omit --token-path while tokenPath is configured, the tokens are written beside the module and the running module will carry on using the stale file at tokenPath.
This matters most on installs that keep MagicMirror on a read-only filesystem and put writable state on a separate partition: tokenPath and --token-path should both point at the writable one.
Each user's tokens are stored as JSON — in whoop_tokens_<userId>.json in the module directory by default, or at
that user's tokenPath if you set one. Users never share a token file.
Both the initial setup.js write and every automatic refresh persist tokens atomically: the new contents are
written to a temporary file in the same directory, flushed to disk, and then renamed over the token file. The token
file is therefore only ever replaced by a complete copy — if the mirror loses power or the process is killed
mid-write, you are left with either the previous tokens or the new ones, never a truncated or empty file. If the
replacement can't be written, the existing token file is left untouched.
Replacement files are created with owner-only permissions (0600) on platforms that support them, so a refresh also
tightens a token file that was previously more permissive.
After the rename the module flushes the containing directory as well, so the replacement itself survives a sudden power loss. Platforms and filesystems with no directory-flush equivalent (Windows, some network mounts) are skipped silently. If a directory flush is supported but fails, the module logs a warning naming the user: the tokens were saved, but that last durability step didn't complete.
Scores are color-coded to match the WHOOP app:
| Metric | Green | Yellow | Red |
|---|---|---|---|
| Recovery | ≥ 67% | 34–66% | ≤ 33% |
| Sleep | ≥ 90% | 75–89% | < 75% |
| Strain | Neutral (white) – not directionally scored |
Token files (whoop_tokens_*.json) contain your OAuth tokens and are excluded from version control via .gitignore. Never commit these files.
The module writes token files with owner-only permissions (0600), and each refresh re-applies them. To tighten a
file written by an older version before its first refresh:
chmod 600 modules/MMM-Whoop/whoop_tokens_*.jsonYou can supply credentials via environment variables instead of config.js:
export WHOOP_CLIENT_ID="your_client_id"
export WHOOP_CLIENT_SECRET="your_client_secret"Then in config.js:
clientId: process.env.WHOOP_CLIENT_ID,
clientSecret: process.env.WHOOP_CLIENT_SECRET,GET /v2/cycle– physiological cycles (day strain)GET /v2/cycle/{id}/recovery– recovery scoresGET /v2/cycle/{id}/sleep– sleep dataGET /v2/activity/workout– workout activitiesPOST /oauth/oauth2/token– token refresh
"No tokens found" – Run the setup.js script for the relevant user.
"Token refresh failed" – Your refresh token may have been revoked. Re-run setup.js for that user.
No data showing – WHOOP data is tied to physiological cycles. If your current cycle hasn't been scored yet (e.g., you haven't slept yet today), some fields will show -- until data is available.
Rate limiting – The default 15-minute interval is well within WHOOP's rate limits for personal use. If you see 429 errors, increase updateInterval.
Activity icons show as boxes – The default uses plain bullet points which work everywhere. If you set useEmoji: true, you need an emoji font installed on the Pi:
sudo apt install fonts-noto-color-emojiThen restart MagicMirror.
Multiple users – Each user needs their own WHOOP account and their own setup.js run. They can share the same WHOOP Developer App (same clientId/clientSecret), but each person authorizes independently.
MIT – see LICENSE for details.
- WHOOP Developer Platform for the API
- MagicMirror² for the platform
