# Sync and read wearable data

Canonical page: https://docs.ovok.com/wearables/sync-and-read-data

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 [#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](#sync-on-demand), 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](/wearables/connect-a-patient#build-the-screen-from-the-list) moves only after the readings are written. A sync that fails leaves it, and the Observations, as they were.

## What each provider syncs [#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`](/errors/sync-not-supported).

## Sync on demand [#sync-on-demand]

```bash
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}"
```

```json
{
  "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`](/errors/sync-too-soon) with `Retry-After` in seconds, and the provider is not asked. Use the [errors table](/wearables/api-reference#sync-a-connection) to decide what the app shows.

## What a wearable Observation looks like [#what-a-wearable-observation-looks-like]

```json
{
  "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 [#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 [#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:**

```bash
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:

```bash
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 [#keep-wearable-and-device-readings-apart]

<Callout type="warn" title="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`](/primitives/last-observations), return both. Unless you filter, the "latest heart rate" on a clinical screen can be a ring's sleep average.

</Callout>

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

```bash
--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.

```bash
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 [#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](/wearables/connect-a-patient) 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.
