API reference
Data model
Latido normalizes every provider into six record families. A developer never converts per provider: the field names and units below are the same whether the data came from Garmin, Oura, Apple Health or the demo provider. The tables at the end of this page are generated from the API's own schemas.
Conventions
- Units are fixed and SI-like: meters, seconds, kilograms, kilocalories, beats per minute,
watts, m/s, milliseconds for HRV (RMSSD), mg/dL for glucose, °C, percentages on 0-100.
The unit is in the field name (
distanceMeters,durationSeconds,weightKg…). - Absent means unknown, never zero. A field the provider did not give is left out: "no data" and "zero" are different answers.
- Time. Instants are ISO 8601 (
startTime,measuredAt…), stored in UTC;utcOffsetMinuteskeeps the member's wall clock. Days (date) are the member's local calendar day,YYYY-MM-DD— for sleep, the day they woke up. - Ids.
idis Latido's (rec_…);providerRecordIdis the provider's own. With the data type,providerRecordIdis unique per connection: a provider correcting a record updates it in place (and raises*.updatedwhere that event exists). - Raw payload. Every record synced from a provider keeps the provider's payload verbatim
(mobile uploads have none). Add
include=raw({ include: "raw" }in the SDK) to a list orGET /v1/records/{id}and each record gainsraw. Webhooks and MCP never carry it. - Sources. For aggregators (Apple Health, Health Connect)
source.appNamenames the app that wrote the record andsource.deviceName/deviceManufacturerthe device.
Reading records
GET /v1/members/{memberId}/{workouts | sleep-sessions | daily-summaries | body-measurements | heart-rate | glucose}
?from=2026-10-01&to=2026-10-27T00:00:00Z&provider=garmin&include=raw&limit=50&cursor=…
GET /v1/records/{recordId}?include=raw- Newest first.
from(inclusive) andto(exclusive) are ISO dates or instants, compared with the record's start. - Pages of
limit(1-200, default 50);meta.nextCursoris absent on the last page. - SDK:
latido.members.workouts.list(memberId, query)for one page,latido.members.workouts.iterate(memberId, query)for all of them;sleepSessions,dailySummaries,bodyMeasurements,heartRate,glucoselikewise. Each family's type is exported (WorkoutRecord,SleepSessionRecord, …), and the SDK checks that every record of a page is of the family asked for.
{
"data": [
{
"id": "rec_V1StGXR8_Z5jdHi6B",
"type": "workout",
"memberId": "mem_…",
"connectionId": "conn_…",
"provider": "garmin",
"providerRecordId": "17234567890",
"sport": "running",
"providerSport": "RUNNING",
"startTime": "2026-10-25T06:00:00.000Z",
"endTime": "2026-10-25T07:00:00.000Z",
"utcOffsetMinutes": 120,
"durationSeconds": 3600,
"distanceMeters": 10000,
"averageHeartRateBpm": 148,
"hasFile": false,
"createdAt": "2026-10-25T07:05:12.000Z",
"updatedAt": "2026-10-25T07:05:12.000Z"
}
],
"meta": { "nextCursor": "eyJ0Ijoi…" }
}Members and connections
- Member (
mem_…): your end user, known by yourexternalUserId(unique per project and mode) with optionalmetadata(≤ 2 KB JSON).GET /v1/members/{id}addsconnectionsandrecordCounts(records stored per data type, every type present). - Connection (
conn_…): one member × one provider —status(pending,active,expired,revoked,error),scopes,providerUserId/providerUserLabel,lastSyncedAt,backfill(status:pending|running|completed|failed,from,completedAt) andlastError(code,message,at). Tokens never leave the backend.
Planned workouts
POST /v1/members/{memberId}/planned-workouts with { provider, workout } sends a
structured workout to a device: name
(≤ 100), sport (SportType), scheduledDate? (YYYY-MM-DD, the member's day),
description?, and 1-100 steps. A step has a kind (warmup, active, recovery,
cooldown, rest, repeat), an optional durationSeconds (≤ 86,400) or distanceMeters,
optional target ranges { min, max } — targetHeartRateBpm, targetPowerWatts,
targetPaceSecondsPerKm — and a note. A repeat step runs its steps (≤ 50) repeat
times (1-100). The answer is queued; GET /v1/planned-workouts/{id} and the
planned_workout.pushed / .failed webhooks report the outcome.
Field reference
Common fields
Every record, whatever its family, carries these.
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
id |
string | yes | Latido's id of the record (rec_…) | |
memberId |
string | yes | The member the record belongs to | |
connectionId |
string | yes | ||
provider |
enum (16 values) | yes | ||
providerRecordId |
string | yes | The provider's own id; with the data type, it is unique per connection | |
source |
{ deviceName, deviceManufacturer, appName } | |||
createdAt |
string (ISO 8601 instant) | yes | When Latido first stored it | |
updatedAt |
string (ISO 8601 instant) | yes | When Latido last changed it |
Workout (workout)
GET /v1/members/{memberId}/workouts · SDK latido.members.workouts
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
type |
"workout" |
yes | ||
sport |
enum (44 values) | yes | ||
providerSport |
string | yes | The provider's own activity type, verbatim | |
name |
string | |||
startTime |
string (ISO 8601 instant) | yes | ||
endTime |
string (ISO 8601 instant) | yes | ||
utcOffsetMinutes |
integer | min | ||
durationSeconds |
number (≥ 0) | yes | s | Elapsed, start to end |
movingSeconds |
number (≥ 0) | s | ||
distanceMeters |
number (≥ 0) | m | ||
elevationGainMeters |
number | m | ||
elevationLossMeters |
number | m | ||
activeKilocalories |
number (≥ 0) | kcal | ||
averageHeartRateBpm |
number | bpm | ||
maxHeartRateBpm |
number | bpm | ||
averageSpeedMps |
number | m/s | ||
maxSpeedMps |
number | m/s | ||
averagePowerWatts |
number | W | ||
normalizedPowerWatts |
number | W | ||
averageCadence |
number | Steps per minute for running, revolutions per minute for cycling | ||
steps |
integer (≥ 0) | |||
trainingLoad |
number | The provider's own load / strain score | ||
perceivedExertion |
number (≥ 1, ≤ 10) | |||
heartRateZones |
{ zone, minBpm, maxBpm, seconds }[] | |||
laps |
{ startTime, durationSeconds, distanceMeters, averageHeartRateBpm, averagePowerWatts, averageSpeedMps }[] | |||
polyline |
string | Encoded polyline of the route, when there is one | ||
startLatitude |
number | |||
startLongitude |
number | |||
isManual |
boolean | |||
hasFile |
boolean | yes | Whether the original file (FIT/GPX/TCX) can be downloaded through Latido |
heartRateZones[] items:
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
zone |
integer (≥ 1, ≤ 5) | yes | ||
minBpm |
number | bpm | ||
maxBpm |
number | bpm | ||
seconds |
number (≥ 0) | yes | s |
laps[] items:
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
startTime |
string (ISO 8601 instant) | yes | ||
durationSeconds |
number (≥ 0) | yes | s | |
distanceMeters |
number (≥ 0) | m | ||
averageHeartRateBpm |
number | bpm | ||
averagePowerWatts |
number | W | ||
averageSpeedMps |
number | m/s |
Daily summary (daily_summary)
GET /v1/members/{memberId}/daily-summaries · SDK latido.members.dailySummaries
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
type |
"daily_summary" |
yes | ||
date |
string (YYYY-MM-DD) | yes | The member's local calendar day | |
steps |
integer (≥ 0) | |||
distanceMeters |
number (≥ 0) | m | ||
floorsClimbed |
number (≥ 0) | |||
activeKilocalories |
number (≥ 0) | kcal | ||
totalKilocalories |
number (≥ 0) | kcal | ||
activeSeconds |
number (≥ 0) | s | ||
sedentarySeconds |
number (≥ 0) | s | ||
restingHeartRateBpm |
number | bpm | ||
averageHeartRateBpm |
number | bpm | ||
maxHeartRateBpm |
number | bpm | ||
hrvRmssdMs |
number | ms (RMSSD) | Daily HRV (RMSSD), usually measured overnight | |
averageStressScore |
number (≥ 0, ≤ 100) | |||
bodyBatteryOrReadiness |
number (≥ 0, ≤ 100) | Garmin Body Battery, Oura readiness, WHOOP recovery… on 0-100 | ||
readinessLabel |
string | The provider's own name for that score | ||
spo2Percent |
number (≥ 0, ≤ 100) | % | ||
respiratoryRateBrpm |
number | breaths/min | ||
skinTemperatureDeviationCelsius |
number | °C | ||
vo2Max |
number |
Sleep session (sleep_session)
GET /v1/members/{memberId}/sleep-sessions · SDK latido.members.sleepSessions
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
type |
"sleep_session" |
yes | ||
date |
string (YYYY-MM-DD) | yes | The day the member woke up | |
startTime |
string (ISO 8601 instant) | yes | ||
endTime |
string (ISO 8601 instant) | yes | ||
utcOffsetMinutes |
integer | min | ||
isNap |
boolean | yes | ||
durationSeconds |
number (≥ 0) | yes | s | In bed, start to end |
asleepSeconds |
number (≥ 0) | s | ||
awakeSeconds |
number (≥ 0) | s | ||
lightSeconds |
number (≥ 0) | s | ||
deepSeconds |
number (≥ 0) | s | ||
remSeconds |
number (≥ 0) | s | ||
latencySeconds |
number (≥ 0) | s | ||
efficiencyPercent |
number (≥ 0, ≤ 100) | % | ||
score |
number (≥ 0, ≤ 100) | The provider's sleep score, 0-100 | ||
averageHeartRateBpm |
number | bpm | ||
lowestHeartRateBpm |
number | bpm | ||
averageHrvRmssdMs |
number | ms (RMSSD) | ||
respiratoryRateBrpm |
number | breaths/min | ||
stages |
{ stage, startTime, endTime }[] |
stages[] items:
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
stage |
awake | light | deep | rem | unknown |
yes | ||
startTime |
string (ISO 8601 instant) | yes | ||
endTime |
string (ISO 8601 instant) | yes |
Body measurement (body_measurement)
GET /v1/members/{memberId}/body-measurements · SDK latido.members.bodyMeasurements
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
type |
"body_measurement" |
yes | ||
measuredAt |
string (ISO 8601 instant) | yes | ||
weightKg |
number (> 0) | kg | ||
bodyFatPercent |
number (≥ 0, ≤ 100) | % | ||
muscleMassKg |
number (> 0) | kg | ||
boneMassKg |
number (> 0) | kg | ||
waterPercent |
number (≥ 0, ≤ 100) | % | ||
bmi |
number (> 0) | |||
heightMeters |
number (> 0) | m | ||
systolicMmHg |
number (> 0) | mmHg | ||
diastolicMmHg |
number (> 0) | mmHg | ||
restingHeartRateBpm |
number | bpm | ||
bodyTemperatureCelsius |
number | °C |
Heart-rate samples (heart_rate_sample)
GET /v1/members/{memberId}/heart-rate · SDK latido.members.heartRate
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
type |
"heart_rate_sample" |
yes | ||
startTime |
string (ISO 8601 instant) | yes | ||
endTime |
string (ISO 8601 instant) | yes | ||
samples |
{ t, bpm }[] | yes | One series per record, so a day of samples is one row, not 86,400 |
samples[] items:
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
t |
integer | yes | s | Seconds since startTime |
bpm |
number | yes | bpm |
Glucose sample (glucose_sample)
GET /v1/members/{memberId}/glucose · SDK latido.members.glucose
| Field | Type | Always present | Unit | Notes |
|---|---|---|---|---|
type |
"glucose_sample" |
yes | ||
measuredAt |
string (ISO 8601 instant) | yes | ||
mgPerDl |
number (> 0) | yes | mg/dL | |
trend |
rising_quickly | rising | flat | falling | falling_quickly | unknown |
Providers and what they carry
From the provider catalog (packages/common/lib/health/provider-catalog.ts). AI is allowsAiProcessing: when no, MCP tools never return the provider's records.
| Slug | Name | Connect | Delivery | Data types | Planned workouts | Max backfill | AI |
|---|---|---|---|---|---|---|---|
garmin |
Garmin | oauth | webhook | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample |
yes | 90 days | yes |
strava |
Strava | oauth | webhook | workout, heart_rate_sample |
365 days | no | |
polar |
Polar | oauth | webhook | workout, daily_summary, sleep_session, heart_rate_sample |
30 days | yes | |
suunto |
Suunto | oauth | webhook | workout, daily_summary, sleep_session |
yes | 365 days | yes |
coros |
COROS | oauth | webhook | workout, daily_summary, sleep_session |
yes | 30 days | yes |
oura |
Oura | oauth | webhook | workout, daily_summary, sleep_session, heart_rate_sample |
365 days | no | |
whoop |
WHOOP | oauth | webhook | workout, daily_summary, sleep_session, body_measurement |
365 days | yes | |
withings |
Withings | oauth | webhook | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample |
365 days | yes | |
fitbit |
Fitbit | oauth | webhook | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample |
365 days | yes | |
google_health |
Google Health | oauth | polling | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample |
365 days | yes | |
wahoo |
Wahoo | oauth | webhook | workout |
yes | 365 days | yes |
dexcom |
Dexcom | oauth | polling | glucose_sample |
90 days | yes | |
huawei |
Huawei Health | oauth | polling | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample |
30 days | yes | |
apple_health |
Apple Health | mobile_sdk | device_upload | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample, glucose_sample |
365 days | yes | |
health_connect |
Health Connect | mobile_sdk | device_upload | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample, glucose_sample |
30 days | yes | |
latido_demo |
Latido Demo | oauth | webhook | workout, daily_summary, sleep_session, body_measurement, heart_rate_sample |
yes | 90 days | yes |
Sports (SportType)
Every provider's activity types map onto these; anything unmapped is other, with the original kept in providerSport.
running, trail_running, treadmill_running, track_running, walking, hiking, cycling, mountain_biking, gravel_cycling, indoor_cycling, virtual_cycling, e_biking, pool_swimming, open_water_swimming, triathlon, rowing, indoor_rowing, kayaking, stand_up_paddling, surfing, cross_country_skiing, alpine_skiing, snowboarding, strength_training, hiit, crossfit, yoga, pilates, elliptical, stair_climbing, climbing, bouldering, tennis, padel, soccer, basketball, golf, martial_arts, boxing, dance, meditation, breathwork, mobility, other