Skip to main content
Which LOINC code is Apple Health’s step count? Is the HRV a Fitbit reports the same statistic as the one an Apple Watch reports? Where does Huawei’s 深睡 land, and is WHOOP’s slow-wave sleep the same thing? No vendor API answers these: none of the thirteen read here carries a LOINC code of its own, and every integration maps by hand and keeps the result to itself. This is that mapping, published, with the half nobody publishes: the 71 device quantities that have no LOINC code, and why. A code you cannot find is a gap in the standard; a code you find wrongly is a wrong number in someone’s chart. The tables were built to make both visible. Every code was checked by hand against the six LOINC 2.83 axes (COMPONENT, PROPERTY, TIME, SYSTEM, SCALE, METHOD) on 2026-09-09. Each row carries a confidence: The catalogue in mirobody/res/catalog/metrics.tsv takes only the confident codes as a metric’s identity. An unverified code stays visible here and in the tables, and the metric keeps its own namespace until someone confirms it.

What is in it

All tables live in mirobody/res/crosswalks/ and ship with pip install mirobody. Loading them:
Rendering them, which is how the tables below were made:

By vendor

Fields read is every field or data type the vendor documents for health data; a field with a code is one the review could place on a LOINC code; confident is the subset that needs no further confirmation. The source column links the document each table was read from, all on 2026-09-09. Garmin publishes only its metric families; field-level schemas need developer approval, so most Garmin rows are unverified until the schema is read. Honor’s service mirrors Huawei’s and its cloud side is read-only. OPPO exposes four values to watch-face scripts and no typed health API; Xiaomi’s public namespace is a workout data cloud with no sleep, blood oxygen, HRV, temperature or stress types.

What no code fits

The reason is the point: it decides what to do with the quantity. The one ALGO_MISMATCH row is the most consequential line in the whole crosswalk, and it is the first trap below. The nine QUALITY_META rows (Fitbit sleep.infoCode and tempSkin.logType, WHOOP score_state, percent_recorded, user_calibrating and total_no_data_time_milli, Oura non_wear_time, Huawei’s ventilator maskOff) are not health data, but they say how far to trust the health data beside them, and they are worth storing next to the reading rather than dropping.

Read this before normalising across vendors

These are the places where two vendors’ numbers look comparable and are not. Each one silently produces a wrong value if ignored.
  1. HRV: one vendor publishes SDNN, five publish RMSSD, four publish nothing. Apple’s heartRateVariabilitySDNN is SDNN. Health Connect’s HeartRateVariabilityRmssdRecord, Huawei’s heartRateVariabilityRMSSD, Fitbit’s hrv.value.dailyRmssd, WHOOP’s recovery.score.hrv_rmssd_milli and Oura’s sleep.average_hrv are RMSSD. Samsung, vivo, Xiaomi and Zepp publish no HRV type. Garmin publishes beat-to-beat RR intervals, from which SDNN can be computed. LOINC 2.83 codes only the SDNN family (112429-6, 76643-6, 80404-7); RMSSD has zero hits in the axis table. The two are different definitions and cannot share a row: the catalogue keeps hrvSDNN (coded), hrvRMSSD (uncoded) and hrvDatas for a series whose statistic is not stated. Syncing an Oura ring into Apple Health writes RMSSD into the field Apple defines as SDNN; a downstream reader that trusts the field name is then wrong.
  2. Skin temperature depends on the site. 61008-9 Body surface temperature (Temp/Qn/Surface) is the quantitative code for a wrist device, marked unverified because the wrist has no code of its own and the code’s IEEE origin is a monitor’s body temperature. 60830-7 Finger temperature is the confident code for a ring (Oura). 60833-1 is the toe. 39106-0 Temperature of Skin is a nominal finding and cannot hold a number; 60839-8 is the infant-incubator context. Health Connect’s SkinTemperatureRecord carries measurementLocation, the one source that lets a decoder pick the site automatically.
  3. Deviation is not temperature. Fitbit’s tempSkin.value.nightlyRelative and Oura’s temperature_deviation are offsets from a personal baseline; WHOOP’s skin_temp_celsius and Health Connect’s baseline are absolute. An offset goes to temperatureDelta, which has no LOINC code and must never be filed under 61008-9 or 8310-5.
  4. Weighted minutes are not minutes. Fitbit active-zone minutes count cardio and peak minutes twice, by the documentation’s own words; Garmin intensity minutes weight vigorous minutes the same way. Undo the weighting before any LOINC duration code, or do not code them.
  5. Energy units. WHOOP’s cycle.score.kilojoule is kilojoules; every other vendor is kilocalories. Divide by 4.184.
  6. Heart-rate zones align nowhere. Fitbit has four, WHOOP five, the catalogue five, LOINC three plus a maximum zone. Not coded; the only comparable quantity across vendors is total moderate-plus-vigorous time.
  7. Activity intensity bands half align. Oura’s low, medium and high map onto LOINC’s light, moderate and vigorous durations (101688-0, 101689-8, 101690-6) and is the one confident case. Health Connect’s ActivityIntensityRecord has two bands and no light. Huawei’s exercise_intensity.v2 is not an intensity band at all; its field is exercise_type, the kind of exercise.
  8. Four vocabularies for the sleep stages. Deep sleep is Apple asleepDeep, Health Connect DEEP, Huawei 深睡, Fitbit deep, WHOOP slow_wave_sleep and Oura deep_sleep_duration; light sleep is Huawei 浅睡, Health Connect LIGHT, Fitbit and Oura light, and Apple asleepCore. All of them land on 93831-6 and 93830-8, but Apple’s Core and WHOOP’s slow-wave sleep are marked unverified: synonymous, not the same name.
  9. The sleep mode is a routing key. Fitbit has two log modes, classic without stages and stages with them. Huawei’s sleep_type has three values: 1 with every field, 2 with no REM, 3 with only a total. Oura’s sleep.type includes late_nap. Route on the mode before coding stages, or a stage the mode does not provide is stored as zero. This is the most dangerous line in this list.
  10. A field name is not its meaning. vivo’s DATA_TYPES.WALKING_SPEED is in steps per minute: a cadence, not a speed. Read the unit column of the vendor’s documentation, never the name.
  11. Statistical capability is a constraint. vivo’s watch API supports minimum and maximum for heart rate, blood oxygen and stress, and sums for standing, intensity, steps, distance and calories, and a mean for nothing. A mean code such as 103205-1 has no vivo source; do not build the mapping because it “should” exist.
  12. Glucose depends on the unit. Honor’s BLOOD_GLUCOSE.measureValue is in mmol/L and codes to 15074-8, not to the catalogue’s 2339-0 (mg/dL). The catalogue alias names the analyte and the printed unit picks the variant; mirobody.translate.code does this for every alias.
  13. Total is not active. Oura’s total_calories and Health Connect’s TotalCaloriesBurnedRecord include basal metabolism; the catalogue’s activeCalories does not. Only the active figure is coded confidently.

LOINC’s own axis defects

The LOINC 2.75 batch that added consumer-device concepts is uneven. Four codes carry axes that contradict their names; every one is marked unverified here, and all four are worth raising with Regenstrief.

How the catalogue uses it

  • A metric’s identity is ("loinc", code) only when the code is confident (Metric.canonical); otherwise it is ("mirobody-device", name), so an unverified code never becomes a series key.
  • The device catalogue’s answer for a metric is handed to mirobody.translate.code as a catalogue alias. The alias names the analyte; when the printed unit fits a sibling of that code in the same specimen, the sibling is used (glucose in mmol/L), and when it fits none, the alias stands.
  • Apple’s SDNN identifier is decoded to hrvSDNN; the Apple gait, perfusion and six-minute-walk identifiers, previously quarantined, now have rows.
  • Five invariants in test_engine_coverage.py hold the tables and the catalogue together: every code is in the axis table with a confidence, no two metrics share a code unless registered as one quantity at two grains, every crosswalk row names a real code and a real catalogue row, the Apple table says what the Apple decoder does, and the unit gate on an alias.

The base table

One row per code. The vendor column lists who produces the quantity; the field names are in the TSV.

Sources

Every table names the document it was read from in its header comment; the same list is mirobody.translate.devices.SOURCES. All were read on 2026-09-09 against LOINC 2.83. LOINC is copyright Regenstrief Institute, Inc., used under the LOINC license; see LICENSE-3RD-PARTY section 3. The codes and the shortened long common names in these tables are LOINC content; the mapping judgements and the notes are this project’s.

Changing it

Edit the TSV and open a pull request. The gate tests reject a code the axis table does not have, a metric name the catalogue does not have, a confident vendor code that contradicts the metric’s confident catalogue code (unless the two are unit variants of one analyte), and an Apple row that disagrees with the Apple decoder. A new vendor is a new <vendor>.tsv, a Source in mirobody.translate.devices, and a column in the base table. Rerun scripts/device_crosswalk_report.py and paste the tables here. Notes inside the tables are in the language they were written in; many are in Chinese, since the review was done against several vendors’ Chinese documentation. The column names and the confidence vocabulary are English.