# Wearable data in FHIR (Apple Health, Health Connect)

> Get smartwatch, ring and scale data into a FHIR record: which devices reach which health store, a LOINC mapping table, daily summaries and a tested upsert.

Source: https://developers.anpheros.com/guides/wearable-data-fhir

**To get data from a smartwatch, ring or smart scale into a medical record, read it on the phone from the operating system's health store — Apple Health on iOS, Health Connect on Android — summarise it per day, map each metric to a LOINC code and a UCUM unit, and write it as FHIR R4 Observations marked as device data.** Reading from the phone's health store instead of from each vendor's cloud lets one integration cover Apple Watch, Galaxy Watch, Pixel Watch, Garmin, Fitbit, Oura and Withings, as long as the person has allowed their vendor app to share. This page gives the data path, a mapping table you can reuse, a validated FHIR example and code that was run against the platform.

## The data path

```
 watch, ring, scale ──► vendor app (Garmin Connect, Fitbit, Oura, Withings, Samsung Health…)
                              │  shares, with the person's permission
                              ▼
                 Apple Health (iOS)  /  Health Connect (Android)
                              │  your app reads, with the person's permission, on the phone
                              ▼
                 your app ──► your backend ──► FHIR R4 Observations in the person's record
```

Neither Apple Health nor Health Connect has a server API: data can only be read by an app on the person's phone. Your app reads it, sends compact daily values to your backend, and the backend writes them to the record.

## Which devices reach which health store

What each vendor shares changes with app versions; the vendor's own documentation is the reference.

| Device or app | Apple Health (iOS) | Health Connect (Android) | Notes |
|---|---|---|---|
| Apple Watch | Yes, natively | — | Everything the watch measures lands in Apple Health |
| Samsung Galaxy Watch, Samsung Health | — | Partly | Samsung Health decides which types it exports and how often; see [Samsung Health](https://developers.anpheros.com/guides/samsung-health-fhir) |
| Pixel Watch, Fitbit | No official sync | Yes, through the Fitbit app ([Google help](https://support.google.com/fitbit/answer/14506680)) | |
| Garmin | Yes, from Garmin Connect after each device sync | Yes, Garmin Connect supports Health Connect on Android 14 and later ([Android Developers, August 2025](https://developer.android.com/health-and-fitness/community/newsletters/2025/08)) | Garmin-specific scores such as Body Battery are not shared |
| Oura | Yes | Yes ([Oura](https://ouraring.com/blog/data-sharing-partner-integrations/)) | Readiness and other Oura scores stay in the Oura app |
| Withings | Yes, through Health Mate | Yes ([Android Developers blog](https://android-developers.googleblog.com/2023/03/withings-reduces-data-sync-code-with--health-and-fitness-api-health-connect.html)) | Scales, blood pressure monitors, watches |

Two practical consequences. First, your app depends on the person having switched sharing on in their vendor app; onboarding should say so. Second, the health store may hold the same steps from two sources (a phone and a watch); read the store's own daily totals rather than adding up samples, because Apple Health and Health Connect already de-duplicate overlapping sources.

## Mapping table: health store type → FHIR Observation

The codes Anpheros Daily uses when it writes wearable data to a FHIR record:

| Metric | Apple HealthKit | Health Connect | LOINC | UCUM unit | Category |
|---|---|---|---|---|---|
| Heart rate | `heartRate` | `HeartRateRecord` | 8867-4 | `/min` | vital-signs |
| Resting heart rate | `restingHeartRate` | `RestingHeartRateRecord` | 40443-4 | `/min` | vital-signs |
| Heart rate variability | `heartRateVariabilitySDNN` | `HeartRateVariabilityRmssdRecord` | 80404-7 (SDNN only) | `ms` | vital-signs |
| Blood oxygen (SpO₂) | `oxygenSaturation` | `OxygenSaturationRecord` | 59408-5 | `%` | vital-signs |
| Respiratory rate | `respiratoryRate` | `RespiratoryRateRecord` | 9279-1 | `/min` | vital-signs |
| Steps per day | `stepCount` | `StepsRecord` | 41950-7 | `{steps}` | activity |
| Active energy | `activeEnergyBurned` | `ActiveCaloriesBurnedRecord` | 41981-2 | `kcal` | activity |
| Sleep duration | `sleepAnalysis` (category) | `SleepSessionRecord` | 93832-4; stages 93831-6 deep, 93830-8 light, 93829-0 REM | `min` | activity |
| Body weight | `bodyMass` | `WeightRecord` | 29463-7 | `kg` | vital-signs |
| Blood pressure | `bloodPressure` (correlation) | `BloodPressureRecord` | 85354-9 with 8480-6 and 8462-4 | `mm[Hg]` | vital-signs |
| Blood glucose (meter) | `bloodGlucose` | `BloodGlucoseRecord` | 2339-0 | `mg/dL` | vital-signs |
| Body temperature | `bodyTemperature` | `BodyTemperatureRecord` | 8310-5 | `Cel` | vital-signs |

Watch the heart rate variability row: Apple Health stores **SDNN**, Health Connect stores **RMSSD**. They are different statistics and must not be written under the same code. LOINC 80404-7 is SDNN; for RMSSD, Anpheros Daily uses its own code system rather than pretending the two are the same.

## Summaries, not every sample

A watch produces thousands of heart-rate samples a day. A medical record needs something a person or a doctor can read:

- **One Observation per metric per day**, with `effectivePeriod` covering the person's local day, the daily average or total in `valueQuantity` and, for rates, the minimum and maximum as components.
- **Finer series stay where they are useful.** Anpheros Daily keeps 4-hour heart-rate summaries for its own charts and writes daily values to the FHIR record.
- **Events are separate.** An irregular-rhythm notification or a single blood pressure reading is its own Observation with its own time, not part of a daily average.

A daily heart-rate summary as FHIR R4 (it passes the [FHIR R4 validator](https://developers.anpheros.com/tools/fhir-validator)):

```json
{
  "resourceType": "Observation",
  "status": "final",
  "identifier": [{"system": "https://your-app.test/fhir/sid/daily-wearable", "value": "p-123:2026-10-01: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"}]},
  "subject": {"reference": "Patient/p-123"},
  "effectivePeriod": {"start": "2026-10-01T00:00:00-04:00", "end": "2026-10-02T00:00:00-04:00"},
  "valueQuantity": {"value": 71.4, "unit": "/min", "system": "http://unitsofmeasure.org", "code": "/min"},
  "component": [
    {"code": {"text": "minimum"}, "valueQuantity": {"value": 52, "unit": "/min", "system": "http://unitsofmeasure.org", "code": "/min"}},
    {"code": {"text": "maximum"}, "valueQuantity": {"value": 138, "unit": "/min", "system": "http://unitsofmeasure.org", "code": "/min"}}
  ],
  "device": {"display": "Apple Watch (via Apple Health)"}
}
```

## Writing daily values without duplicates

Phones sync several times a day, and the last value of a day keeps changing until midnight. Give each Observation a stable identifier — person, day, metric — and update it instead of creating a new one. With the Anpheros TypeScript SDK, on your backend:

```ts
import { Anpheros, apiKey } from '@anpheros/sdk';

const anpheros = new Anpheros({ auth: apiKey(process.env.ANPHEROS_KEY!) });

// What the phone sends: one value per metric per local day
type DailyValue = { metric: 'heart_rate' | 'steps'; day: string; value: number; min?: number; max?: number };

const CODES = {
  heart_rate: { code: '8867-4', display: 'Heart rate', unit: '/min', category: 'vital-signs' },
  steps: { code: '41950-7', display: 'Number of steps in 24 hour Measured', unit: '{steps}', category: 'activity' },
} as const;

// One Observation per person, metric and day: re-sending a day updates it instead of adding a copy.
async function upsertDaily(patientId: string, v: DailyValue, source: string) {
  const c = CODES[v.metric];
  const identifier = { system: 'https://your-app.test/fhir/sid/daily-wearable', value: `${patientId}:${v.day}:${v.metric}` };
  const resource: any = {
    resourceType: 'Observation', status: 'final',
    identifier: [identifier],
    category: [{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: c.category }] }],
    code: { coding: [{ system: 'http://loinc.org', code: c.code, display: c.display }] },
    subject: { reference: `Patient/${patientId}` },
    effectivePeriod: { start: `${v.day}T00:00:00Z`, end: `${v.day}T23:59:59Z` },
    valueQuantity: { value: v.value, unit: c.unit, system: 'http://unitsofmeasure.org', code: c.unit },
  };
  if (v.min !== undefined && v.max !== undefined) {
    resource.component = [
      { code: { text: 'minimum' }, valueQuantity: { value: v.min, unit: c.unit, system: 'http://unitsofmeasure.org', code: c.unit } },
      { code: { text: 'maximum' }, valueQuantity: { value: v.max, unit: c.unit, system: 'http://unitsofmeasure.org', code: c.unit } },
    ];
  }
  const found = await anpheros.fhir.search('Observation', { identifier: `${identifier.system}|${identifier.value}` });
  const existing = found.entry?.[0]?.resource;
  return existing
    ? anpheros.fhir.update({ ...resource, id: existing.id }, { author_type: 'device' })
    : anpheros.fhir.create(resource, { author_type: 'device', source_system: source });
}
```

Run against a local instance of the platform with a sandbox key, two daily values followed by a later sync of the same day's steps left **two** Observations, the steps one at version 2 with the new value, and provenance `v1 device apple-health · v2 device`. The history keeps the earlier value, and every reader can see that the data came from a device, not from a clinician.

In production, use the real day boundaries in the person's time zone (the example above uses UTC for brevity), as the HealthKit and Health Connect code on the platform pages does.

## Reading on the phone

- **iPhone and Apple Watch:** [Apple Health (HealthKit) to FHIR](https://developers.anpheros.com/guides/apple-health-fhir), with Swift code for daily statistics.
- **Android, Wear OS, Fitbit, Garmin, Oura, Withings:** [Health Connect to FHIR](https://developers.anpheros.com/guides/health-connect-fhir), with Flutter code that works on both platforms.
- **Samsung Galaxy Watch and Samsung Health:** [Samsung Health to FHIR](https://developers.anpheros.com/guides/samsung-health-fhir): what reaches Health Connect and what needs Samsung's own SDK.

## Rules worth following

- **Read only what you use**, and ask for each type separately; people grant partial permissions.
- **Label the source.** Consumer wearables are not diagnostic devices. Write their data as device data (`author_type: device`) and say which app it came from, so a clinician can weigh it accordingly.
- **Respect the platform rules.** Apple's App Review Guidelines (5.1.3) forbid using health data for advertising and storing personal health information in iCloud; Google Play requires a health apps declaration for apps that use Health Connect.
- **Let people disconnect** and decide whether the data already copied stays in the record.

## What Anpheros does and does not do

The Anpheros Platform does not connect to Apple Health, Health Connect or any vendor cloud by itself: your app reads the data on the phone and writes it through the API. Anpheros' own app, **Anpheros Daily**, does exactly that: it reads Apple Health on iPhone and Health Connect on Android, keeps daily summaries and writes them to the person's FHIR record with the codes above. Connecting devices is part of its Plus and Max plans.

## Frequently asked questions

### How do I get Apple Watch data into a FHIR server?
Read it on the iPhone from Apple Health with HealthKit (there is no server API), summarise it per day, and have your backend write each metric as an Observation with a LOINC code and UCUM unit, marked as device data.

### Is there a cloud API for Health Connect or Apple Health?
No. Both keep data on the phone; an app installed on the phone reads it with the person's permission. Vendor clouds such as Garmin's, Fitbit's or Oura's have their own APIs, but reading the phone's health store covers most of them with one integration.

### Which LOINC code should I use for steps?
41950-7, "Number of steps in 24 hour Measured", with a daily `effectivePeriod`. For heart rate use 8867-4, for SpO₂ 59408-5, for sleep duration 93832-4.

### Should I store every heart-rate sample in FHIR?
Usually not. A daily summary with average, minimum and maximum is what a person or clinician reads; individual readings belong in the record only when they matter on their own, such as an irregular-rhythm event.

### Can I use one code for HRV from Apple Health and Health Connect?
No. Apple Health reports SDNN and Health Connect reports RMSSD; they measure different things and need different codes.

## Related

- [How to build a personal health record app](https://developers.anpheros.com/guides/personal-health-record-app)
- [Healthcare integrations](https://developers.anpheros.com/guides/healthcare-integrations): devices next to labs, clinics and apps
- [FHIR code systems](https://developers.anpheros.com/guides/fhir-code-systems): LOINC and UCUM
- [Medical data provenance explained](https://developers.anpheros.com/guides/medical-data-provenance)
- [How to normalize medical data](https://developers.anpheros.com/guides/normalize-medical-data)
- [Remote patient monitoring with FHIR](https://developers.anpheros.com/guides/remote-patient-monitoring-fhir): individual readings, webhooks and alerts
