Files
HealthLog/docs/integrations/whoop.md
T
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00

93 lines
3.8 KiB
Markdown

# WHOOP integration
HealthLog connects to WHOOP via OAuth2 plus HMAC-signed webhooks for
near-real-time sync of recovery, strain, sleep, and the underlying
heart-rate / HRV signals. Unlike Withings, WHOOP credentials are
brought per user: each user (or the operator on their behalf)
registers their own WHOOP developer app and pastes the client
id/secret into Settings.
## Why bring your own keys
WHOOP caps the number of users a single developer app may authorise.
Per-user BYO-keys sidestep that per-app cap: every user authorises
against their own WHOOP app, so a shared HealthLog instance never
runs into one app's user ceiling. It also keeps each user's WHOOP
grant scoped to credentials they control.
## 1. Register a WHOOP developer app
1. Sign in at <https://developer.whoop.com/> and create a new app.
2. **Redirect URI:** `https://your-instance.example.com/api/whoop/callback`
(matches `WHOOP_REDIRECT_URI`, below). Add one entry per
environment you connect from.
3. Request these scopes (the exact set HealthLog asks for):
```
offline read:recovery read:sleep read:workout read:cycles read:profile read:body_measurement
```
`offline` is **mandatory** — without it WHOOP issues no refresh token
and the connection dies when the first access token expires (~1 hour).
The remaining `read:*` scopes are read-only; request all of them so the
user grants once and every sync resource is covered.
4. Save the app. WHOOP issues a **Client ID** and **Client Secret** —
keep the secret out of any chat log; it grants read access to the
linked account's full WHOOP history.
## 2. Wire the credentials in Settings → Integrations
WHOOP credentials are stored per user, not in `.env`. Sign in as the
target user, open **Settings → Integrations → WHOOP**, and paste the
client ID and secret. They are encrypted AES-256-GCM at rest.
## 3. Connect
Click **Connect WHOOP**. The flow:
1. The Connect button redirects to WHOOP's OAuth authorize endpoint.
2. The user signs in on WHOOP and grants the requested scopes.
3. WHOOP redirects back to `/api/whoop/callback` with an auth code.
4. HealthLog exchanges the code for an access + refresh token, stores
them encrypted, registers the webhook, and triggers an initial
sync of recent history.
5. Subsequent syncs run on every webhook delivery plus a safety-net
cron pull.
## Optional instance env vars
Two server-level env vars tune the OAuth callback and webhook
verification. They are optional — the per-user client id/secret in
Settings drive the connection itself.
```env
WHOOP_REDIRECT_URI="https://your-instance.example.com/api/whoop/callback"
WHOOP_WEBHOOK_SECRET="$(openssl rand -hex 32)"
```
- `WHOOP_REDIRECT_URI` — the OAuth callback URL handed to WHOOP. Must
match a redirect URI registered in the WHOOP developer app.
- `WHOOP_WEBHOOK_SECRET` — used twice: as the **trailing path segment**
of the webhook URL you register in the WHOOP console, and as the key
HealthLog verifies the WHOOP HMAC body signature against. Set the
webhook URL in the WHOOP console to:
```
https://your-instance.example.com/api/whoop/webhook/<WHOOP_WEBHOOK_SECRET>
```
WHOOP signs each body (`X-WHOOP-Signature` + `X-WHOOP-Signature-Timestamp`,
HMAC-SHA256); the handler rejects deliveries older than five minutes and
validates the signature before processing. The body signing key on WHOOP's
side must equal `WHOOP_WEBHOOK_SECRET`. Generate a fresh random value and
restart the `app` container after setting it. Without a webhook, WHOOP
still syncs on the safety-net cron pull — only the near-real-time push is
lost.
## Disconnect
The Settings page **Disconnect** button revokes the stored tokens and
removes the connection. Existing measurements synced from WHOOP stay
in the database — disconnect does not delete data.