Sync and read wearable data

When wearable data reaches your project, what each provider syncs, what the Observations look like, and how to query them apart from device readings.

Ovok pulls each connection's readings from the provider and writes them as Observation resources about the Patient, in the Patient's project. Your app reads them like any other FHIR data.

When data arrives

  • Daily at 08:00 UTC, for every connection, after the night's sleep has reached the providers.
  • On notification, for Withings, Polar and Suunto: when the provider tells Ovok a patient has new data, Ovok syncs that connection about 30 seconds later. Notifications within one 30-second window share one sync. A notification only starts a sync; it never carries data, so the window and the writes are the same as any other sync.
  • On demand, when the patient's app calls the sync route, at most once every 15 minutes per connection.

What a sync reads:

SyncWindow
The first one after connectingThe last 30 days
Every later oneFrom 2 days before syncedThrough, because providers revise recent records such as steps and sleep scores
After a long gapNever further back than 30 days. Days older than that are not fetched.

syncedThrough on the connection moves only after the readings are written. A sync that fails leaves it, and the Observations, as they were.

What each provider syncs

MetricOuraWHOOPStravaWithingsPolarUltrahumanSuunto
Heart rateSleep averageCycle averageWorkout averageEach measurement; night averageNight average (nightly recharge)
Resting heart rateSleep lowestRecoveryDuring sleep
HRV (RMSSD)Sleep averageRecoveryNightNight (nightly recharge)
SpO₂DailyRecoveryEach measurementDaily, when given as one value
Respiratory rateSleep averageSleepNight averageNight breathing rate (nightly recharge)
StepsDailyDailyDailyDailyDaily
Sleep duration and stagesEach sleep periodEach sleepEach nightEach nightEach night

Heart rate is not the same kind of value everywhere: a sleep or nightly average for Oura, Withings and Polar (Withings adds single measurements), the day's average for WHOOP, and one workout's average for Strava. Read effectivePeriod before comparing values.

  • Oura: rest periods and periods the user deleted are not sleep and are skipped.
  • WHOOP: only records WHOOP has scored are synced; an unscored or still-open record arrives with a later sync. WHOOP has no step count. Sleep duration is light + deep + REM.
  • Strava: records workouts only. A workout without heart rate gives nothing.
  • Withings: reads measurements, daily activity and the nightly sleep summary, not the high-frequency sleep series.
  • Polar: lists sleep and nightly recharge for its last 28 days only, so a first sync holds 28 days of those and 30 of steps. Nights are dated by the night; steps by the watch's local day. Polar's access cannot be refreshed: once Polar stops accepting it, the connection needs reconnecting.
  • Ultrahuman: per-sample heart rate and HRV are not synced, and there is no respiratory rate. Days are UTC dates. Ovok asks one day at a time, so a first sync makes 31 requests; days with no data are skipped. Built without partner access, so metrics may be missing.
  • Suunto: daily steps only, until Ovok has partner access. When two sources report one day, the highest count is kept.

Garmin can be connected but is not synced yet: its sync answers 501 SYNC_NOT_SUPPORTED.

Sync on demand

export OVOK_TOKEN='<access token of a patient>'

curl --request POST \
  --url 'https://api.sandbox.ovok.com/v1/wearables/connections/oura/sync' \
  --header "Authorization: Bearer ${OVOK_TOKEN}"
{
  "provider": "oura",
  "created": 42,
  "syncedThrough": "2026-10-22T08:00:00.000Z"
}

created counts the Observations this sync created. Readings Ovok already holds are updated in place and not counted, so a repeated sync answers created: 0.

The cooldown counts from syncedThrough, so it also applies just after the daily sync. Within it, the answer is 429 SYNC_TOO_SOON with Retry-After in seconds, and the provider is not asked. Use the errors table to decide what the app shows.

What a wearable Observation looks like

{
  "resourceType": "Observation",
  "status": "final",
  "meta": {
    "project": "<project id>",
    "tag": [{ "system": "https://api.ovok.com/fhir/CodeSystem/wearables-source", "code": "oura" }]
  },
  "identifier": [
    { "system": "ovok-event-id", "value": "wearable:oura:<patient id>:<record id>:heart_rate" }
  ],
  "category": [
    { "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "vital-signs" }] }
  ],
  "code": {
    "coding": [{ "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" }],
    "text": "Heart rate"
  },
  "subject": { "reference": "Patient/<patient id>" },
  "device": { "display": "Oura" },
  "effectivePeriod": { "start": "2026-10-21T23:12:00+02:00", "end": "2026-10-22T07:03:00+02:00" },
  "valueQuantity": { "value": 54, "system": "http://unitsofmeasure.org", "code": "/min", "unit": "/min" }
}
FieldWhat it holds
meta.tagThe source: system https://api.ovok.com/fhir/CodeSystem/wearables-source, code the provider (oura, whoop, strava, withings, polar, ultrahuman, suunto). Use it to find or exclude wearable data.
identifierSystem ovok-event-id, value wearable:<provider>:<patient id>:<record id>:<metric>. One per reading: when the provider revises a value, the same Observation is updated.
devicedisplay only: the provider's name. There is no Device resource.
effectiveDateTime or effectivePeriodA period for anything measured over time (a sleep, a cycle, a workout). A date such as 2026-10-21 for daily values such as steps. A date-time for single measurements, such as WHOOP recovery or a Withings reading.
categoryvital-signs or activity, per the table below.

Codes and units

MetricCodeUnit (UCUM)Category
Heart rateLOINC 8867-4 Heart rate/minvital-signs
Resting heart rateLOINC 40443-4 Heart rate --resting/minvital-signs
HRV (RMSSD)https://api.ovok.com/fhir/CodeSystem/wearables hrv-rmssdmsvital-signs
SpO₂LOINC 59408-5 Oxygen saturation in Arterial blood by Pulse oximetry%vital-signs
Respiratory rateLOINC 9279-1 Respiratory rate/minvital-signs
StepsLOINC 41950-7 Number of steps in 24 hour Measured{steps}activity
Sleep durationLOINC 93832-4 Sleep durationsactivity
Light sleepLOINC 93830-8 Light sleep durationsactivity
Deep sleepLOINC 93831-6 Deep sleep durationsactivity
REM sleepLOINC 93829-0 REM sleep durationsactivity
Awake timeLOINC 93828-2 Nighttime awakening durationsactivity

LOINC has no code for RMSSD (80404-7 is SDNN), so HRV uses Ovok's own code system.

Read the data

All examples use a practitioner or patient token that can read the Patient's Observations.

Everything one provider synced for a patient:

curl --get 'https://api.sandbox.ovok.com/fhir/R4/Observation' \
  --header "Authorization: Bearer ${OVOK_TOKEN}" \
  --data-urlencode "subject=Patient/${PATIENT_ID}" \
  --data-urlencode '_tag=https://api.ovok.com/fhir/CodeSystem/wearables-source|oura' \
  --data-urlencode '_sort=-date'

One metric from any wearable, for example nightly HRV:

curl --get 'https://api.sandbox.ovok.com/fhir/R4/Observation' \
  --header "Authorization: Bearer ${OVOK_TOKEN}" \
  --data-urlencode "subject=Patient/${PATIENT_ID}" \
  --data-urlencode 'code=https://api.ovok.com/fhir/CodeSystem/wearables|hrv-rmssd' \
  --data-urlencode 'date=ge2026-10-01'

Keep wearable and device readings apart

Heart rate from a wearable and from a device share one code

A wearable heart rate is LOINC 8867-4, like a heart rate from a connected device. A plain Observation search on that code, and $lastn, return both. Unless you filter, the "latest heart rate" on a clinical screen can be a ring's sleep average.

Only wearable readings: add _tag with the provider, comma-separated for several:

--data-urlencode '_tag=https://api.ovok.com/fhir/CodeSystem/wearables-source|oura,https://api.ovok.com/fhir/CodeSystem/wearables-source|withings'

Everything except wearable readings: add _tag:not with every provider.

curl --get 'https://api.sandbox.ovok.com/fhir/R4/Observation/$lastn' \
  --header "Authorization: Bearer ${OVOK_TOKEN}" \
  --data-urlencode "patient=Patient/${PATIENT_ID}" \
  --data-urlencode 'code=8867-4' \
  --data-urlencode '_tag:not=https://api.ovok.com/fhir/CodeSystem/wearables-source|oura,https://api.ovok.com/fhir/CodeSystem/wearables-source|whoop,https://api.ovok.com/fhir/CodeSystem/wearables-source|strava,https://api.ovok.com/fhir/CodeSystem/wearables-source|withings,https://api.ovok.com/fhir/CodeSystem/wearables-source|polar,https://api.ovok.com/fhir/CodeSystem/wearables-source|suunto,https://api.ovok.com/fhir/CodeSystem/wearables-source|garmin,https://api.ovok.com/fhir/CodeSystem/wearables-source|ultrahuman'

$lastn passes extra search parameters such as _tag through to the search. List all eight providers in _tag:not, Garmin included, so the filter keeps working when Garmin starts syncing.

What else to know

  • Not forwarded to Signals. Wearable Observations are never sent for Signals alert evaluation, whether or not the patient is enrolled.
  • Disconnecting keeps the data. Observations already written stay. Delete them yourself if your retention rules require it; their ovok-event-id identifier names the provider.
  • Reconnect required. When the provider refuses the stored grant, for example after the patient removed your app's access, Ovok marks the connection reconnectRequired and stops syncing it. The patient connects again to resume; the next sync catches up on at most 30 days.
  • Values follow the provider. Ovok stores what the provider reports, converted to the units above. It does not validate, smooth or clinically review readings.

Last updated on

On this page