Integrations

Whoop integration

OAuth, sync schedule, schema, and how the brief uses recovery data.

Updated · 2026-05-28

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 endpointMayva tableNotes
/v2/recoveryhealth_signals kind=recoveryScore 0-100, HRV, RHR
/v2/sleephealth_signals kind=sleepDuration, efficiency, stages
/v2/cyclehealth_signals kind=cycleDaily cycle bounds
/v2/strainhealth_signals kind=strainDay strain, kJ
/v2/workoutworkoutsPer-workout strain, HR zones
/v2/body-measurementhealth_signals kind=bodyWeight, 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 STALE on the Whoop status pulse.
  • The morning brief sees no fresh recovery and falls back to its none tone.

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.