WHOOP API Documentation

WHOOP v2 integration for Vitals7

Overview

Connects a Vitals7 user's WHOOP account, stores OAuth tokens in DynamoDB vitals-di-tokens (key {cognitoSub}#whoop), and writes recovery, sleep, cycle and workout readings to user_vitals.

Base URL: http://localhost:8084
WHOOP API: https://api.prod.whoop.com/developer (v2)
Webhook URL: https://dev-whoop-api.vitals7.com/webhook

Authentication

OAuth 2.0. The state parameter carries the Vitals7 Cognito user id, and offline scope is required to receive a refresh token.

GET/login?state=<cognitoSub>
Redirects to WHOOP for consent. Optional app_redirect=vitals7://… for the mobile app.
GET/callback
Exchanges the code for tokens, saves them, responds immediately, then backfills 30 days in the background.
GET/api/whoop/auth/initiate?state=<cognitoSub>
Returns the authorization URL as JSON instead of redirecting.
Dashboard setup: the Redirect URL and Webhook URL must match WHOOP_REDIRECT_URI and WHOOP_WEBHOOK_URL exactly, and the webhook Model Version must be v2.

API Endpoints

GET/health?deep=1
Service + OAuth config status; deep=1 also probes DynamoDB.
GET/api/whoop/status?userId=<cognitoSub>
Connection state, WHOOP user id, last sync time and row count.
GET/api/whoop/data?userId=<cognitoSub>&days=30
Live WHOOP fetch (cycles, sleeps, recoveries, workouts, profile, body) without writing.
POST/api/whoop/sync
Body {"cognitoUserId":"…","days":30}. Fetches and writes to user_vitals.
POST/api/whoop/reconcile
Runs one reconciliation pass over all connected users immediately.
POST/api/whoop/disconnect
Body {"cognitoUserId":"…"}. Revokes WHOOP access (stopping webhooks) and deletes the token row.

Webhooks

WHOOP posts {user_id, id, type, trace_id} for sleep.updated, sleep.deleted, recovery.updated, recovery.deleted, workout.updated and workout.deleted. Each request is signature-checked, de-duplicated by trace_id, answered 204 immediately, then synced on a debounced per-user queue.

POST/webhook
Primary endpoint (alias: /webhook/whoop). GET/HEAD return 200 for health checks.

Signature: base64(HMAC_SHA256(X-WHOOP-Signature-Timestamp + rawBody, WHOOP_CLIENT_SECRET)) compared against X-WHOOP-Signature. Requests older than WHOOP_WEBHOOK_MAX_SKEW_MS (default 5 min) are rejected.

Because deliveries can be missed, a reconciliation pass also runs every 60 min — set WHOOP_RECONCILE_INTERVAL_MIN.

Data mapping

WHOOPvitalTypeunits
recovery.resting_heart_rateheart-ratebpm
recovery.hrv_rmssd_milliheart-rate-variabilityms
recovery.spo2_percentageoxygen-saturation%
recovery.skin_temp_celsiusbody-temperature°F
recovery.recovery_scorerecovery-score%
sleep stage summary (light+SWS+REM)sleep-hourshours
sleep.respiratory_raterespiratory-ratebreaths/min
cycle.strainstrainscore
cycle / workout average_heart_rateheart-ratebpm
cycle / workout kilojouletotal-calorieskcal
workout.distance_meterdistancekm
workout durationexercise-durationminutes
body.weight_kilogramweightlbs
body.height_meterheightinches
weight + heightbmikg/m²
Only unscored records are skipped (score_state !== 'SCORED'). Naps are excluded from sleep-hours unless WHOOP_INCLUDE_NAPS=1.

Examples

# Connect
open "http://localhost:8084/login?state=YOUR_COGNITO_SUB"

# Status
curl "http://localhost:8084/api/whoop/status?userId=YOUR_COGNITO_SUB"

# Manual sync
curl -X POST "http://localhost:8084/api/whoop/sync" \
  -H "Content-Type: application/json" \
  -d '{"cognitoUserId":"YOUR_COGNITO_SUB","days":30}'

# Reconcile everyone now
curl -X POST "http://localhost:8084/api/whoop/reconcile"

# Disconnect
curl -X POST "http://localhost:8084/api/whoop/disconnect" \
  -H "Content-Type: application/json" \
  -d '{"cognitoUserId":"YOUR_COGNITO_SUB"}'