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:
| Sync | Window |
|---|---|
| The first one after connecting | The last 30 days |
| Every later one | From 2 days before syncedThrough, because providers revise recent records such as steps and sleep scores |
| After a long gap | Never 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
| Metric | Oura | WHOOP | Strava | Withings | Polar | Ultrahuman | Suunto |
|---|---|---|---|---|---|---|---|
| Heart rate | Sleep average | Cycle average | Workout average | Each measurement; night average | Night average (nightly recharge) | ||
| Resting heart rate | Sleep lowest | Recovery | During sleep | ||||
| HRV (RMSSD) | Sleep average | Recovery | Night | Night (nightly recharge) | |||
| SpO₂ | Daily | Recovery | Each measurement | Daily, when given as one value | |||
| Respiratory rate | Sleep average | Sleep | Night average | Night breathing rate (nightly recharge) | |||
| Steps | Daily | Daily | Daily | Daily | Daily | ||
| Sleep duration and stages | Each sleep period | Each sleep | Each night | Each night | Each 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" }
}| Field | What it holds |
|---|---|
meta.tag | The 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. |
identifier | System 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. |
device | display only: the provider's name. There is no Device resource. |
effectiveDateTime or effectivePeriod | A 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. |
category | vital-signs or activity, per the table below. |
Codes and units
| Metric | Code | Unit (UCUM) | Category |
|---|---|---|---|
| Heart rate | LOINC 8867-4 Heart rate | /min | vital-signs |
| Resting heart rate | LOINC 40443-4 Heart rate --resting | /min | vital-signs |
| HRV (RMSSD) | https://api.ovok.com/fhir/CodeSystem/wearables hrv-rmssd | ms | vital-signs |
| SpO₂ | LOINC 59408-5 Oxygen saturation in Arterial blood by Pulse oximetry | % | vital-signs |
| Respiratory rate | LOINC 9279-1 Respiratory rate | /min | vital-signs |
| Steps | LOINC 41950-7 Number of steps in 24 hour Measured | {steps} | activity |
| Sleep duration | LOINC 93832-4 Sleep duration | s | activity |
| Light sleep | LOINC 93830-8 Light sleep duration | s | activity |
| Deep sleep | LOINC 93831-6 Deep sleep duration | s | activity |
| REM sleep | LOINC 93829-0 REM sleep duration | s | activity |
| Awake time | LOINC 93828-2 Nighttime awakening duration | s | activity |
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-ididentifier 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
reconnectRequiredand 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