- name
- whoop-fitness
- version
- 0.1.0
- description
- WHOOP fitness integration — daily sleep/recovery/strain profiles, token auto-refresh, and summary delivery via Hermes stdout.
- triggers
- ["whoop","recovery","sleep score","strain","fitness data","workout data"]
# WHOOP Fitness Integration
Daily WHOOP data sync with automatic token refresh and summary delivery via Hermes. Provides contextual awareness of the user's sleep, recovery, strain, and workout history.
## How It Works
1. **Daily cron** runs at **12:00 UTC** (= 8am EDT / 7am EST). Cron expression: `0 12 * * *`. ET run time shifts by 1 hour with DST because the cron schedule is fixed in UTC. → fetches WHOOP data → outputs summary to stdout (captured by Hermes for delivery)
2. **Token auto-refresh** using `offline` scope refresh token (no re-auth needed)
3. **Profile file** at `/opt/data/whoop/daily_profile.json` is readable by Hermes for contextual awareness
## Files
```
/opt/data/whoop/
├── tokens.json # OAuth tokens (access + refresh + client credentials)
├── daily_profile.json # Latest profile summary (JSON: {timestamp, summary_text})
├── daily_profile_raw.json # Latest raw API data (JSON)
skills/custom/whoop-fitness/scripts/
├── whoop_daily.py # Main daily sync script (cron target)
├── whoop_query.py # On-demand Q&A data fetcher
└── run_coach.py # WHOOP-powered running coach
Install: symlink or copy scripts into `/opt/data/scripts/` for cron access.
```
## Setup
### 1. Register WHOOP App
1. Go to https://developer.whoop.com
2. Create an app with these scopes:
- `read:recovery`, `read:sleep`, `read:workout`, `read:cycles`, `read:profile`, `read:body_measurement`, `offline`
3. Set redirect URI to: `https://<your-fly-app>.fly.dev/callback`
### 2. Initial OAuth Authorization
Generate auth URL:
```
https://api.prod.whoop.com/oauth/oauth2/auth?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<REDIRECT_URI>&scope=read%3Arecovery+read%3Asleep+read%3Aworkout+read%3Acycles+read%3Aprofile+read%3Abody_measurement+offline&state=<random_8_chars>
```
Exchange code for tokens (atomic — token never exposed to LLM due to redaction):
```bash
curl -s -X POST https://api.prod.whoop.com/oauth/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<CODE>" \
-d "client_id=<CLIENT_ID>" \
-d "client_secret=<CLIENT_SECRET>" \
-d "redirect_uri=<REDIRECT_URI>" | python3 -c "
import sys, json, os
d = json.load(sys.stdin)
path = '/opt/data/whoop/tokens.json'
os.makedirs(os.path.dirname(path), exist_ok=True)
open(path,'w').write(json.dumps({
'access_token': d['access_token'],
'refresh_token': d.get('refresh_token',''),
'expires_at': '',
'client_id': '<CLIENT_ID>',
'client_secret': '<CLIENT_SECRET>',
'redirect_uri': '<REDIRECT_URI>'
}, indent=2))
os.chmod(path, 0o600)
print(f'Saved. refresh_token: {len(d.get(\"refresh_token\",\"\"))} chars')
"
```
### 3. Set Up Cron Job
```bash
# In Hermes (8am ET = 12pm UTC during EDT):
cronjob create --name "WHOOP Daily Profile" --schedule "0 12 * * *" --script whoop_daily.py
```
**Note:** Cron is hardcoded to UTC. Adjust for DST: 12pm UTC = 8am EDT, 1pm UTC = 8am EST.
## API Details
- **Base URL:** `https://api.prod.whoop.com/developer`
- **Auth:** OAuth 2.0 with `offline` scope for refresh tokens
- **Token expiry:** 1 hour (auto-refreshed after 401 response)
- **Endpoints used:**
- `GET /v2/user/profile/basic` — name, email
- `GET /v2/user/measurement/body` — height, weight, max HR
- `GET /v2/recovery?limit=14` — recovery score, HRV, RHR, SpO2
- `GET /v2/activity/sleep?limit=14` — sleep stages, performance, efficiency, wake time (end field)
- `GET /v2/activity/workout?limit=14` — strain, HR, duration, sport type
- `GET /v2/cycle?limit=14` — daily strain, calories
> **Note:** The daily cron (`whoop_daily.py`) and run coach (`run_coach.py`) fetch `limit=14` (cycles) to `limit=30` (recovery/sleep/workout). On-demand Q&A (`whoop_query.py`) defaults to 30 days, configurable via argument.
## Token Refresh Flow
```
On fetch:
1. Attempt all API calls with current access_token
2. If any endpoint returns 401:
a. POST to /oauth/oauth2/token with grant_type=refresh_token
b. Save new access_token (keep existing refresh_token if none returned)
c. Retry full fetch with new token
d. If refresh fails → write error profile JSON with re-auth URL (built from stored redirect_uri)
```
## Pitfalls
- **Secret redaction:** Hermes redacts tokens in terminal output (`security.redact_secrets: true`). All token handling must be atomic (curl | python3 → file). Never pass tokens through LLM context. Use `execute_code` or single shell pipelines.
- **Cloudflare blocking:** WHOOP API blocks bare `urllib` requests (error 1010). Add `User-Agent: Mozilla/5.0 hermes-whoop/1.0` header, or use `curl` instead.
- **urllib timeout:** Use `timeout=30` (single int), NOT `timeout=(5, 30)` (tuple is for `requests` library).
- **Token file location:** `/opt/data/whoop/tokens.json` (not .env — avoids redaction issues).
- **Offline scope:** Must be included in both the app settings AND the auth URL. Not listed in OpenAPI spec but documented at `https://developer.whoop.com/docs/developing/oauth`.
- **API base URL:** `https://api.prod.whoop.com/developer` (not just `/whoop.com`). Discovered via OpenAPI spec at `/developer/doc/openapi.json`.
- **Auth codes are single-use:** Each code can only be exchanged once. If the exchange fails or the token gets redacted, generate a fresh auth link.
- **OAuth state parameter:** WHOOP requires `state` with minimum 8 characters.
- **Redirect URI must match exactly:** Including path (`/callback` vs `/whoop-callback`) — no trailing slash.
- **Cron timezone:** Cron uses UTC. 8am ET = 12pm UTC (EDT) or 1pm UTC (EST). Adjust seasonally.
## Wake Time Tracking
Sleep records include `end` time which is the user's wake-up time. Scripts convert from UTC to ET using `zoneinfo.ZoneInfo('America/New_York')` (DST-aware). Typical wake window: 7:50–9:00 AM ET.
## Q&A Support
When Rishi asks fitness questions, use the WHOOP data to answer. Two approaches:
### 1. Use cached data (fast)
Read `/opt/data/whoop/daily_profile_raw.json` — contains data from the last fetch. The daily cron (`whoop_daily.py`) fetches the last 14 records per endpoint; on-demand runs via `whoop_query.py` default to 30 days (configurable). Check the file's timestamp to confirm freshness.
### 2. Fetch fresh data (if needed)
```bash
python3 /opt/data/scripts/whoop_query.py 30 # fetches last 30 days
```
Outputs JSON to stdout. Also updates the cache file.
### Question Types & How to Answer
| Type | Example | Approach |
|------|---------|----------|
| **State** | "How's my recovery?" | Read latest recovery from cache |
| **Trend** | "Is my HRV improving?" | Compare 7-day vs 30-day averages |
| **Causal** | "Did my run affect sleep?" | Correlate workout date → next day sleep |
| **Comparison** | "Runs vs lifts — what recovers better?" | Group workouts by type, avg next-day recovery |
| **Advice** | "Should I work out today?" | Check recovery + recent strain + sleep debt |
| **Specific** | "How much did I sleep Tuesday?" | Filter sleep by date |
| **Aggregate** | "Total miles this month?" | Sum workout distances |
### Coaching Rules
1. **Lead with the answer**, then explain the data
2. **Be honest** — if data is insufficient, say so
3. **Give actionable advice** — not just numbers
4. **Use context** — consider day of week, recent patterns, sleep debt
5. **Keep it concise** — 3-5 lines max unless asked for detail
### Example Q&A
**Q:** "Should I run today?"
**A:** "Recovery is 61 🟡. You ran yesterday (strain 13.2) and sleep debt is 1.8hrs. Swap for Z2 stairmaster or rest. If you do run, 30min max, stay in Z2."
**Q:** "Do I sleep better after runs or lifts?"
**A:** "Last 30 days: after runs — avg sleep perf 84%, 7h 12m. After lifts — 78%, 6h 48m. Run days give you better sleep, especially morning runs. Evening lifts past 7pm correlate with worst sleep."
**Q:** "Am I overtraining?"
**A:** "Checking... Last 7 days: 6 workouts, avg strain 11.4. Recovery declined 4 of 5 days (72→41). Sleep debt climbing. HRV dipped to 38ms (avg 52ms). ⚠️ Yes, looks like overreaching. Take 2 rest days."
## What WHOOP API Cannot Do
- ❌ Create/edit/delete workouts
- ❌ Read Strength Trainer exercise data (sets, reps, weights)
- ❌ Write any data back to WHOOP
- Exercise tracking must be done manually (voice input or separate system)
# Last reviewed: Mon May 11 18:11:13 UTC 2026
عرض على GitHub