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.
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 |
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 |
curl -s "https://vitaltrends.net/api/v1/apple-health/daily-summary?date=2026-04-21" \
-H "Authorization: Bearer YOUR_API_KEY" | jq .
{
"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"
}
]
}
}
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).
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 .
{
"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.
curl -s "https://vitaltrends.net/api/v1/whoop/recovery-status" \
-H "Authorization: Bearer YOUR_API_KEY" | jq .
{
"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
curl -s "https://vitaltrends.net/api/v1/whoop/daily?start=2024-01-01&per_page=7" \
-H "Authorization: Bearer YOUR_API_KEY" | jq .
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
{
"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 and we will consider adding it.