A Python application that scrapes health data from the Whoop API and stores it in PostgreSQL. Designed for containerized deployments with database-backed token storage and optional encryption.
- OAuth2 authentication with automatic token refresh
- Database-backed token storage (persists across container restarts)
- Optional Fernet encryption for tokens at rest
- Scrapes all Whoop API endpoints:
- User profile and body measurements
- Physiological cycles (strain data)
- Recovery scores
- Sleep data (including naps)
- Workouts
- Stores data in PostgreSQL with upsert support
- CLI interface for authorization and scraping
- Kubernetes CronJob ready
# Clone the repository
git clone https://github.com/mischavandenburg/whoop-scraper.git
cd whoop-scraper
# Install dependencies with uv
uv sync
# Or with mise
mise install
mise exec -- uv syncdocker pull ghcr.io/mischavandenburg/whoop-scraper:latestEnvironment variables:
| Variable | Required | Description |
|---|---|---|
WHOOP_CLIENT_ID |
Yes | OAuth2 client ID from Whoop Developer Portal |
WHOOP_CLIENT_SECRET |
Yes | OAuth2 client secret |
WHOOP_DB_HOST |
Yes | PostgreSQL host |
WHOOP_DB_PORT |
No | PostgreSQL port (default: 5432) |
WHOOP_DB_NAME |
Yes | Database name |
WHOOP_DB_USER |
Yes | Database user |
WHOOP_DB_PASSWORD |
Yes | Database password |
WHOOP_SCRAPE_DAYS |
No | Days of history to scrape (default: 7) |
WHOOP_ACCESS_TOKEN |
No | Initial access token for bootstrap |
WHOOP_REFRESH_TOKEN |
No | Initial refresh token for bootstrap |
WHOOP_ENCRYPTION_KEY |
No | Fernet key for encrypting tokens in database |
- Create an application at https://developer.whoop.com/
- Set redirect URI to
http://localhost:8080/callback - Copy your Client ID and Client Secret
# Set credentials
export WHOOP_CLIENT_ID='your-client-id'
export WHOOP_CLIENT_SECRET='your-client-secret'
# Run OAuth flow (opens browser)
whoop-scraper auth
# Check token status
whoop-scraper auth --status
# Test API connection
whoop-scraper test-api# Print SQL schema
whoop-scraper init-db --print-sql
# Initialize schema (requires DB connection)
whoop-scraper init-db# Scrape last 7 days (default)
whoop-scraper scrape
# Scrape last 30 days
whoop-scraper scrape --days 30The scraper automatically handles token refresh:
- On startup, loads tokens from database (or env vars for initial bootstrap)
- Before each API call, checks if access token is expired (with 5-min buffer)
- If expired, uses refresh token to get new tokens from Whoop
- New tokens (both access AND refresh) are saved to database
- This ensures tokens persist across container restarts
For production deployments, enable encryption for tokens stored in the database:
# Generate a Fernet encryption key
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# Set the encryption key
export WHOOP_ENCRYPTION_KEY='your-generated-key'When WHOOP_ENCRYPTION_KEY is set:
- Tokens are encrypted with Fernet (AES-128-CBC) before storing in PostgreSQL
- Tokens are decrypted when loaded from the database
- The encryption key should be stored securely (e.g., Azure Key Vault, AWS Secrets Manager)
apiVersion: batch/v1
kind: CronJob
metadata:
name: whoop-scraper
spec:
schedule: "0 6 * * *" # Daily at 6 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: whoop-scraper
image: ghcr.io/mischavandenburg/whoop-scraper:latest
args: ["scrape", "--days", "7"]
env:
- name: WHOOP_CLIENT_ID
valueFrom:
secretKeyRef:
name: whoop-secrets
key: client-id
- name: WHOOP_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: whoop-secrets
key: client-secret
- name: WHOOP_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: whoop-secrets
key: encryption-key
- name: WHOOP_DB_HOST
value: "postgres.database.svc.cluster.local"
- name: WHOOP_DB_NAME
value: "whoop"
- name: WHOOP_DB_USER
valueFrom:
secretKeyRef:
name: whoop-db-secrets
key: username
- name: WHOOP_DB_PASSWORD
valueFrom:
secretKeyRef:
name: whoop-db-secrets
key: password
restartPolicy: OnFailureFor the first run in Kubernetes:
- Run OAuth flow locally to get initial tokens
- Store tokens in your secrets manager (e.g., Azure Key Vault)
- Set
WHOOP_ACCESS_TOKENandWHOOP_REFRESH_TOKENenv vars for initial bootstrap - After first successful refresh, tokens are stored in database
- Subsequent runs use database tokens (env vars only needed for bootstrap)
The scraper creates these tables:
whoop_oauth_tokens- OAuth tokens (single row, encrypted)whoop_user_profile- User profile informationwhoop_body_measurement- Height, weight, max heart ratewhoop_cycle- Daily physiological cycles with strain scoreswhoop_recovery- Recovery scores, HRV, resting heart ratewhoop_sleep- Sleep sessions with stage breakdownswhoop_workout- Workout activities with heart rate zones
# Install dev dependencies
uv sync --all-extras
# Run linting
uv run ruff check .
# Run type checking
uv run mypy src/
# Run tests
uv run pytestMIT