Stridee
Stridee Docs

Wellness

Sleep, daily activity, HRV, stress and fitness summaries — what arrives, what it contains, and why corrections are a separate event.

Wellness is what your user's watch measured without being asked: sleep, steps, HRV, stress. It arrives like activities do — pushed to your endpoint, with a listing for anything you missed — with one difference: a summary can change after we tell you about it. Garmin sends a night's sleep provisionally and settles it hours later.

The kinds

kindWhat it isGrain
dailySteps, distance, calories, resting heart rate, intensity minutesOne per day
sleepOne sleep: stages, duration, scoreSeveral per day — a nap is its own record
hrvHeart-rate variability — overnight on Garmin (see the COROS note below)Usually one per day
stressAll-day stress, and the body battery derived from itOne per day
fitnessVO₂max and fitness ageOne per day, no measurement window
epochFifteen minutes of one activity typeMany per day
respirationAll-day breathing rateOne per window
pulse_oxBlood oxygen saturation, all-day or on demandSeveral per day
body_compositionA weigh-in: weight, body fat, muscle mass, BMIOne per weigh-in
blood_pressureOne cuff reading: systolic, diastolic, pulseOne per reading
skin_temperatureOvernight skin temperature, as a deviation from baselineUsually one per day
health_snapshotA two-minute on-demand test reporting several metrics at onceOne per test

kind is our name, the same whichever watch sent it. Which provider sends which is below.

  • Don't assume one row per day. A nap is its own sleep record, so key your storage on id, not on user, kind and date.
  • fitness has no window. duration_seconds is null and start_time is a placeholder at midnight UTC. vo2_max is absent on most days; fitness_age usually isn't.

What an event looks like

wellness.created
{
  "id": "3b8e1c94-7a52-4d06-b19f-5c2e07a4d831",
  "type": "wellness.created",
  "created": "2026-08-25T07:02:11Z",
  "webhook_id": "2d7b45c1-8e0a-4f36-b512-9c7d3a6e04f8",
  "nonce": "Kd4nWpLbEa9xTvRm2Cj7Lz0Bq2vNhCz7",
  "user_id": "4c9a7e15-6d3b-42f8-91c0-8e5b2a7d04f6",
  "provider": "garmin",
  "data": {
    "object": "wellness",
    "id": "3f07d5b2-9e14-42a8-8c63-1d5a0be93f72",
    "kind": "sleep",
    "provider_summary_id": "x9f2c1e7-4b83-4d21",
    "calendar_date": "2026-08-25",
    "start_time": "2026-08-24T22:41:00Z",
    "duration_seconds": 24908,
    "tz_offset_seconds": 7200,
    "revision": 1,
    "finality": "tentative",
    "metrics": {
      "total_sleep_seconds": 24908,
      "deep_sleep_seconds": 5820,
      "light_sleep_seconds": 13140,
      "rem_sleep_seconds": 4560,
      "awake_seconds": 1388
    },
    "summary": {
      "durationInSeconds": 24908,
      "deepSleepDurationInSeconds": 5820,
      "lightSleepDurationInSeconds": 13140,
      "remSleepInSeconds": 4560,
      "awakeDurationInSeconds": 1388,
      "validation": "AUTO_TENTATIVE"
    },
    "series_url": "https://api.stridee.com/v1/wellness/3f07d5b2-9e14-42a8-8c63-1d5a0be93f72/series"
  }
}

data.id is our id for this summary, and yours alone. Store against it.

  • metrics is normalized — total_sleep_seconds means the same on every provider. Only the keys this kind carries are present, so a sleep record has no steps key at all.
  • summary is the provider's own payload, minus its time series, with the provider's field names. Use it for anything we haven't promoted to a metric; it changes when the provider changes it.

Corrections, and the one way to get this wrong

When a summary changes you get wellness.updated with the same data.id and a higher revision. Identical re-sends emit nothing.

Handle wellness.updated, or you keep Garmin's tentative sleep forever. Nothing errors — the number is just wrong.

Upsert on data.id, keeping the higher revision:

JavaScript
if (type === 'wellness.created' || type === 'wellness.updated') {
  await db.wellness.upsert({
    where: { id: data.id },
    create: data,
    // Deliveries can repeat and arrive out of order; an older revision is a no-op.
    update: { ...data, where: { revision: { lt: data.revision } } },
  });
}

finality is tentative, final, or null for kinds without the notion — only sleep uses it. A tentative reading never overwrites a final one.

Time series are a separate call

Minute-by-minute data — heart-rate samples, the sleep-stage map, the stress curve — is most of the bytes and rarely needed, so it is behind series_url, a signed call that returns the provider's series unmodified:

Shell
GET /v1/wellness/3f07d5b2-9e14-42a8-8c63-1d5a0be93f72/series

It is null when there is nothing to fetch — every fitness record and everything from COROS. That is not an error.

Listing what you were sent

Shell
GET /v1/wellness?kind=sleep&from_date=2026-08-01&to_date=2026-08-31&limit=50
Parameter
user_idOne athlete, by our id for them.
providerAny provider slug. An unknown one is an empty page, not an error.
kindOne of the kinds above. An unknown one is a 400.
from_date / to_dateCalendar days, inclusive both ends. What the measurement is about.
since / untilRFC 3339 instants, half-open. When we told you.
starting_afterThe id of the last row on the previous page. See Paging.
limitDefaults to 50, capped at 200.

Display by calendar_date, reconcile by since/until. calendar_date is what the measurement is about; received_at is when you were granted it, and only that one moves forward — a night from March can arrive today. Each row also has revised_at, or null.

What each provider sends

ProviderKindsNotes
GarminAll of themPayloads passed through untouched.
COROSdaily, sleep, hrvBuilt by us from one daily query. daily has steps and calories (resting HR is only in summary.rhr); sleep has duration only; hrv_avg is COROS's ppgHrv, method undocumented. No series.
ZeppsleepFull stages, score, finality and series — the same shape as Garmin.
Fitbitdaily, sleep, hrv, respiration, pulse_oxVia the Google Health API; summary is Google's object, int64s as strings. daily has resting HR only; sleep has stages but no score; the others are nightly values anchored to midnight UTC.
Polar, Wahoo, HammerheadNoneNo wellness records at all.

Access follows the same rule as activities: granted when a summary arrives, never for a period your user was disconnected, and not taken back when they disconnect. History from before they connected is included, as far back as each provider allows.

Something wrong or missing on this page? Tell us in Discord. Need something the API doesn’t do yet? Request it on the roadmap.