Integrations
Whoop integration
OAuth, sync schedule, schema, and how the brief uses recovery data.
Mayva ingests Whoop data — recovery, sleep, cycle, strain, workouts, body measurements — and the morning brief reads the recovery band to adjust its tone.
Setup
pnpm whoop:auth
Opens your browser, redirects to Whoop’s OAuth consent screen, hits a localhost callback, drops the refresh token into the local DB. The token is encrypted at rest in whoop_tokens with a per-install key.
You only do this once. The sync worker handles refresh from then on.
What gets synced
whoop-sync runs every 15 minutes. Each run pulls anything newer than the last successful run:
| Whoop V2 endpoint | Mayva table | Notes |
|---|---|---|
/v2/recovery | health_signals kind=recovery | Score 0-100, HRV, RHR |
/v2/sleep | health_signals kind=sleep | Duration, efficiency, stages |
/v2/cycle | health_signals kind=cycle | Daily cycle bounds |
/v2/strain | health_signals kind=strain | Day strain, kJ |
/v2/workout | workouts | Per-workout strain, HR zones |
/v2/body-measurement | health_signals kind=body | Weight, height (rarely changes) |
Every row stored is the raw API payload plus a recorded_at timestamp. We don’t transform on write — the readers in packages/core/memory/health.ts do whatever interpretation is needed at read time.
Backfill
The first sync is unlikely to grab everything you want. For a clean history:
pnpm whoop:backfill --days 90
Pulls the last 90 days into the local DB. Idempotent — re-running won’t dupe rows (matched on Whoop’s id).
How the morning brief uses it
packages/core/agent/morning-brief.ts:
const recovery = await getRecoveryForDate(today);
const band = recovery
? bandFor(recovery.score) // 'green' | 'yellow' | 'red'
: 'none';
const tone = TONE_BY_BAND[band]; // changes the system prompt
The bands roughly correspond to Whoop’s published thresholds (33% / 66%). On a yellow day the brief says “protect deep work, defer hard meetings.” On a red day, it adds “this is a protective day.” On a green day, it doesn’t editorialize.
Sub-6h sleep gets the same protective treatment as red recovery, even if recovery is yellow. That logic is in bandFor — easy to change.
When Whoop is down
The whoop-sync worker is contractually no-throw. If Whoop’s API is unreachable:
- The worker logs the error and returns ok.
- The dashboard shows
STALEon the Whoop status pulse. - The morning brief sees no fresh recovery and falls back to its
nonetone.
There is never an exception path that prevents the brief from running.
Privacy
- Whoop access tokens are encrypted at rest. They never leave your machine in self-host mode.
- On hosted plans, the tokens are encrypted per-tenant with a key derived from a KMS master.
- Whoop data is never sent to Anthropic raw — the brief composer sends only the summary numbers (recovery score, sleep hours) it actually needs.
Disabling
You can disable Whoop entirely without uninstalling:
echo "WHOOP_ENABLED=false" >> .env
The brief will skip the recovery section. The HealthSummary card on Today will show NO SIGNAL instead of a gauge.
To revoke the OAuth grant: log into Whoop’s web app and revoke “Mayva”. Tokens go dead immediately; the sync worker starts failing closed within 15 minutes.