# REST API v1

The VitalTrends API lets you query your own health data programmatically. Build scripts, notebooks, automations, or pipe your data into any tool you like.

> **Pro feature.** API access requires an active VitalTrends subscription. Generate your API key in Settings → Developer after logging in at https://vitaltrends.net/login.

## Authentication

All API requests must include your API key in the `Authorization` header:

```
Authorization: Bearer YOUR_API_KEY
```

Keep your API key secret. Regenerate it from the Developer settings page if it is ever exposed.

## Base URL

```
https://vitaltrends.net/api/v1
```

All public REST endpoints are versioned by URL path. The current stable version is `v1`.

All endpoints return JSON. Dates are ISO 8601 strings (`YYYY-MM-DD`). Timestamps are UTC unless a field is explicitly documented as a local date.

## Versioning

| Field | Value |
|-------|-------|
| API version | `v1` |
| Docs version | `2026-05-09` |
| Base path | `/api/v1` |
| Stability | Additive fields may be added to `v1`; breaking response or behavior changes require a new versioned path such as `/api/v2`. |

### v1 changelog

| Date | Change |
|------|--------|
| `2026-05-19` | `/workouts/unified` now includes stable `session_id`, `source_records`, and `field_sources` fields so clients can show one physical session while preserving raw provider provenance. Add `include=heart_rate_series` to return per-source HR overlays on session rows. |
| `2026-05-11` | `/whoop/daily` now includes the current local-day open cycle when present and adds an additive `is_partial` flag. Closed-cycle fields keep their existing meaning. |
| `2026-05-09` | Hevy workout endpoints added: `/workouts/unified` now includes Hevy, and `/workouts/hevy` returns Hevy workouts with exercises and sets. |
| `2026-05-09` | `/whoop/recovery-status` added recovery freshness timestamps for agents and automations; `/whoop/daily` now includes recovery sync/change timestamps. |
| `2026-05-08` | Oura data endpoints added for daily sleep, sleep sessions, readiness, activity, workouts, SpO2, stress, resilience, and VO2 max. |
| `2026-05-05` | Date-only `start` and `end` filters on timestamp-backed list endpoints now cover the full calendar day in the user's profile timezone. |
| `2026-05-05` | `/whoop/daily` added `cycle_start_date` and `cycle_end_date` while keeping the legacy `date` field unchanged. |
| `2026-05-05` | Rate limit documentation corrected to match the active API limiter: 120 requests per minute per API key. |

## Endpoints

### Workouts

| Method | Path | Description |
|--------|------|-------------|
| GET | `/workouts/unified` | Cross-source workout feed with WHOOP, Apple Health, Oura, Strava, and Hevy deduplicated into sessions. Each row includes `session_id`, canonical provider fields, `source_records`, and `field_sources`; append `include=heart_rate_series` for per-source HR overlay samples. |
| GET | `/workouts/hevy` | Hevy workout records with nested exercises and sets |

```bash
curl -s "https://vitaltrends.net/api/v1/workouts/hevy?start=2026-05-01&per_page=10" \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .
```

### WHOOP

| Method | Path | Description |
|--------|------|-------------|
| GET | `/whoop/daily` | Recovery, HRV, RHR, sleep score, and strain per day |
| GET | `/whoop/recovery-status` | Latest recovery freshness, sync, and upstream update timestamps |
| GET | `/whoop/workouts` | Workout records with sport, duration, and heart rate zones |
| GET | `/whoop/sleep` | Sleep sessions with stages and performance scores |

### Oura

| Method | Path | Description |
|--------|------|-------------|
| GET | `/oura/daily-sleep` | Daily sleep scores and sleep score contributors |
| GET | `/oura/sleep` | Sleep sessions with stages, duration, HRV, heart rate, and respiratory rate |
| GET | `/oura/daily-readiness` | Daily readiness scores, temperature deviation, and readiness contributors |
| GET | `/oura/daily-activity` | Activity scores, calories, steps, distance, active minutes, and activity contributors |
| GET | `/oura/workouts` | Workout sessions with activity, intensity, source, calories, distance, and timestamps |
| GET | `/oura/daily-spo2` | Daily SpO2 averages and breathing disturbance index |
| GET | `/oura/daily-stress` | Daily stress, recovery, and Oura day summary |
| GET | `/oura/daily-resilience` | Daily resilience level and resilience contributors |
| GET | `/oura/vo2-max` | VO2 max estimates by day |

### Withings

| Method | Path | Description |
|--------|------|-------------|
| GET | `/withings/measurements` | Weight, body fat, muscle mass, and bone mass readings |

### Apple Health

| Method | Path | Description |
|--------|------|-------------|
| GET | `/apple-health/daily-summary` | Everything for a single day in one call: activity, heart, sleep stages, body, workouts |
| GET | `/apple-health/daily` | Paginated daily totals across a date range (legacy wide-table shape) |
| GET | `/apple-health?type=<type>` | Per-type time series. Append `&include=metadata` to surface sleep stages and workout details. |

#### Daily summary, one request for a full picture

Use this when you want the morning-dashboard shape: every metric for a given date, including a sleep stage breakdown (`deep`, `rem`, `core`, `awake`, `in_bed`) and full workout metadata (sport, duration, HR min/avg/max, energy, flights climbed).

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `date` | date | today (UTC) | Single day, `YYYY-MM-DD` |
| `types` | csv | all | Subset to include. Any of: `activity`, `heart`, `sleep`, `body`, `workouts` |

```bash
curl -s "https://vitaltrends.net/api/v1/apple-health/daily-summary?date=2026-04-21" \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .
```

```json
{
  "data": {
    "date": "2026-04-21",
    "activity": {
      "steps": 12500,
      "distance_km": 8.234,
      "active_energy_kcal": 487.2,
      "basal_energy_kcal": 1789.4,
      "apple_move_time_min": 45,
      "apple_exercise_time_min": 28,
      "apple_stand_time_min": 180,
      "flights_climbed": 12,
      "time_in_daylight_min": 87,
      "apple_stand_hours": 12
    },
    "heart": {
      "avg_heart_rate": 68,
      "min_heart_rate": 52,
      "max_heart_rate": 134,
      "resting_heart_rate": 56,
      "hrv_ms": 45.2,
      "respiratory_rate": 14.2,
      "spo2_pct": 97.8,
      "vo2_max": 42.5
    },
    "sleep": {
      "total_minutes": 452,
      "start_time": "2026-04-20T23:14:00+00:00",
      "end_time": "2026-04-21T06:46:00+00:00",
      "stages": {
        "deep_min": 68,
        "rem_min": 92,
        "core_min": 256,
        "light_min": 0,
        "awake_min": 36,
        "in_bed_min": 480,
        "unknown_min": 0
      },
      "sources": ["Apple Watch"]
    },
    "body": {
      "weight_kg": 75.20,
      "body_fat_pct": 18.40,
      "lean_body_mass_kg": 61.30,
      "bmi": 23.4,
      "height_m": 1.79,
      "body_comp_date": "2026-04-18"
    },
    "workouts": [
      {
        "uuid": "018f0a3b-1c7e-7d9a-8a2b-8c5c47b3d401",
        "type": "Functional Strength Training",
        "sport_key": "functional_strength_training",
        "start_time": "2026-04-21T07:02:00+00:00",
        "end_time": "2026-04-21T07:47:00+00:00",
        "duration_min": 45,
        "active_energy_kcal": 320,
        "distance_m": null,
        "avg_heart_rate": 132,
        "min_heart_rate": 85,
        "max_heart_rate": 165,
        "flights_climbed": 3,
        "elevation_gain_m": null,
        "source": "Apple Watch"
      }
    ]
  }
}
```

> **Multi-source.** When both iPhone and Apple Watch record the same metric (e.g. steps), the summary returns the larger per-source total instead of summing them, so two devices never double-count.

#### Per-type time series, with optional metadata

Use this when you want raw samples, a date range, or per-segment sleep stages. Pass `include=metadata` to reveal the `metadata` field (sleep stage, workout type, labels).

```bash
curl -s "https://vitaltrends.net/api/v1/apple-health?type=sleep&start=2026-04-20&end=2026-04-21&include=metadata" \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .
```

```json
{
  "data": [
    {
      "data_type": "sleep",
      "date": "2026-04-20",
      "start_time": "2026-04-21T01:45:00+00:00",
      "end_time": "2026-04-21T05:45:00+00:00",
      "value": 0,
      "value_min": null,
      "value_max": null,
      "sample_count": 1,
      "unit": "hr",
      "source": "Apple Watch",
      "metadata": { "stage": "core" }
    },
    {
      "data_type": "sleep",
      "date": "2026-04-20",
      "start_time": "2026-04-20T23:15:00+00:00",
      "end_time": "2026-04-21T00:15:00+00:00",
      "value": 0,
      "value_min": null,
      "value_max": null,
      "sample_count": 1,
      "unit": "hr",
      "source": "Apple Watch",
      "metadata": { "stage": "deep" }
    }
  ],
  "links": { "first": "...", "last": null, "prev": null, "next": null },
  "meta": { "current_page": 1, "from": 1, "path": "...", "per_page": 50, "to": 2 }
}
```

#### Valid `type` values

| Category | Values |
|----------|--------|
| Activity | `steps`, `distance_walking_running`, `active_energy_burned`, `basal_energy_burned`, `apple_move_time`, `apple_stand_hour`, `time_in_daylight`, `physical_effort`, `swimming_stroke_count_qty`, `workout_effort_score`, `estimated_workout_effort_score` |
| Heart & vitals | `heart_rate`, `resting_heart_rate`, `heart_rate_variability`, `heart_rate_recovery`, `respiratory_rate`, `oxygen_saturation`, `blood_pressure_systolic`, `blood_pressure_diastolic`, `blood_glucose`, `body_temperature`, `afib_burden` |
| Sleep | `sleep`, `apple_sleeping_breathing_disturbances`, `sleep_apnea_event` |
| Workouts | `workouts` |
| Body composition | `body_mass`, `body_fat_percentage`, `lean_body_mass`, `body_mass_index`, `height` |
| Mobility | `walking_double_support`, `walking_steadiness`, `stair_ascent_speed`, `stair_descent_speed`, `six_minute_walk_distance` |
| Running & cycling | `running_power`, `cycling_power`, `cycling_speed` |
| Water sports | `underwater_depth`, `water_temperature`, `distance_paddle_sports`, `paddle_sports_speed`, `distance_rowing`, `rowing_speed` |
| Winter sports | `distance_cross_country_skiing`, `cross_country_skiing_speed`, `distance_skating_sports` |
| Fitness events | `low_cardio_fitness_event` |

Availability depends on which HealthKit sensors and iOS version your device supports. Types that are defined but not yet ingested by the VitalTrends iOS companion app are listed in `meta.unavailable_types` on the daily summary response.

## Query parameters

All list endpoints support the following parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `start` | date or datetime | none | Inclusive lower bound. Date-only values use `YYYY-MM-DD`. |
| `end` | date or datetime | none | Inclusive upper bound. Date-only values use `YYYY-MM-DD`. |
| `per_page` | integer | 50 | Records per page (max 200) |
| `page` | integer | 1 | Page number (1-indexed) |

For timestamp-backed endpoints, date-only `start` and `end` values are interpreted as full calendar-day boundaries in your profile timezone. Date-backed Apple Health aggregate endpoints and Oura daily endpoints compare against the stored `date` value directly.

On `/whoop/daily`, `date` remains the legacy cycle-end date for closed cycles; use `cycle_start_date` and `cycle_end_date` when you need explicit local cycle dates. If the user's current local-day cycle is still open, the endpoint may include it with `is_partial: true` and `cycle_end_date: null`. For morning automations, `/whoop/daily` is the canonical endpoint for deciding whether today's WHOOP recovery and sleep row is usable: select the row where `cycle_start_date` equals the user's local date and require both `recovery_score` and `sleep_duration_minutes`.

### WHOOP recovery freshness

Use `/whoop/recovery-status` as advisory sync metadata only. It is useful when an automation needs to know whether VitalTrends recently checked, changed, or received upstream recovery data, but it is not a current-day completeness gate. Its `latest_recovery` and `latest_cycle` summaries use raw WHOOP cycle timestamps and do not apply the `/whoop/daily` sleep wake-day normalization for open cycles. `last_recovery_data_changed_at` advances only when stored recovery values change. `last_recovery_synced_at` advances when VitalTrends checks/applies recovery data, even if values are unchanged.

```bash
curl -s "https://vitaltrends.net/api/v1/whoop/recovery-status" \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .
```

```json
{
  "data": {
    "connected": true,
    "needs_reconnection": false,
    "latest_recovery": {
      "cycle_id": "123e4567-e89b-12d3-a456-426614174000",
      "cycle_start_date": "2026-05-08",
      "cycle_end_date": "2026-05-09",
      "recovery_score": 82,
      "hrv_rmssd_milli": 68.4,
      "resting_heart_rate": 48,
      "recovery_synced_at": "2026-05-09T07:06:00+00:00",
      "recovery_data_changed_at": "2026-05-09T07:06:00+00:00",
      "whoop_recovery_updated_at": "2026-05-09T07:05:00+00:00"
    },
    "latest_cycle": {
      "cycle_id": "123e4567-e89b-12d3-a456-426614174000",
      "cycle_start_date": "2026-05-08",
      "cycle_end_date": "2026-05-09",
      "recovery_score": 82,
      "hrv_rmssd_milli": 68.4,
      "resting_heart_rate": 48,
      "recovery_synced_at": "2026-05-09T07:06:00+00:00",
      "recovery_data_changed_at": "2026-05-09T07:06:00+00:00",
      "whoop_recovery_updated_at": "2026-05-09T07:05:00+00:00"
    },
    "last_recovery_synced_at": "2026-05-09T07:06:00+00:00",
    "last_recovery_data_changed_at": "2026-05-09T07:06:00+00:00",
    "last_whoop_recovery_updated_at": "2026-05-09T07:05:00+00:00",
    "is_recovery_data_stale": false,
    "stale_after_hours": 24
  }
}
```

If `is_recovery_data_stale` is true or `last_recovery_data_changed_at` is more than 24 hours old, treat recovery freshness as uncertain and try again later. Do not conclude that today's recovery or sleep row is missing until you have checked `/whoop/daily` for the user's current local `cycle_start_date`. Opening the VitalTrends WHOOP dashboard also triggers a small recent-window WHOOP refresh when the latest recovery appears stale.

WHOOP daily `date` is the legacy cycle-end date for closed cycles; use `cycle_start_date`, `cycle_end_date`, and `is_partial` when you need explicit local cycle boundaries. WHOOP workout fields ending in `_milli` are durations in milliseconds, and `kilojoule` is energy in kilojoules.

Oura duration fields are returned in the units provided by Oura. Sleep durations are seconds; workout distance is meters; calorie fields are kilocalories.

On `/oura/workouts`, `source` is Oura's own workout source value. For example, `confirmed` means the workout was saved in Oura after confirmation in the Oura app. It is not a VitalTrends confidence score or a cross-device verification status. Other common values include `autodetected`, `manual`, and `workout_heart_rate`.

Withings measurement fields ending in `_kg` are stored in kilograms, and `fat_ratio_pct` is a percentage. Empty Withings fields mean the connected device did not provide that metric for the measurement, not that VitalTrends rejected it.

Apple Health `source` is the HealthKit device or app label that wrote the sample, not a confidence or verification status. Pass `include=metadata` on `/apple-health?type=<type>` to see sleep stages and workout details. For cumulative daily metrics such as steps, distance, and energy, VitalTrends avoids double-counting by choosing the largest per-source daily total by default.

## Example request

```bash
curl -s "https://vitaltrends.net/api/v1/whoop/daily?start=2024-01-01&per_page=7" \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .
```

```bash
curl -s "https://vitaltrends.net/api/v1/oura/daily-readiness?start=2026-05-01&per_page=7" \
  -H "Authorization: Bearer YOUR_API_KEY" | jq .
```

### Example response

```json
{
  "data": [
    {
      "date": "2024-01-07",
      "cycle_start_date": "2024-01-06",
      "cycle_end_date": "2024-01-07",
      "is_partial": false,
      "recovery_score": 82,
      "hrv_rmssd_milli": 68.4,
      "resting_heart_rate": 48,
      "sleep_performance_pct": 89,
      "sleep_duration_minutes": 450,
      "strain": 12.4,
      "whoop_recovery_updated_at": "2024-01-07T07:05:00+00:00",
      "recovery_synced_at": "2024-01-07T07:06:00+00:00",
      "recovery_data_changed_at": "2024-01-07T07:06:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 7,
    "total": 7,
    "from": 1,
    "to": 7
  }
}
```

## Rate limits

The API allows **120 requests per minute** per API key. If you exceed the limit, requests return `429 Too Many Requests` with a `Retry-After` header.

## Error responses

| Status | Meaning |
|--------|---------|
| `401` | Missing or invalid API key |
| `403` | Valid key but subscription required |
| `422` | Invalid query parameters |
| `429` | Rate limit exceeded |
| `500` | Server error, try again shortly |

---

Need an endpoint that is not listed here? Get in touch at https://vitaltrends.net/contact and we will consider adding it.
