What the oxygen saturation biomarkers measure
| Biomarker | What it is |
|---|---|
oxygen_saturation | The day’s average blood oxygen saturation across all readings |
oxygen_saturation_sleep | The average across the hours the user was in bed |
| Field | oxygen_saturation, oxygen_saturation_sleep |
|---|---|
| Unit | Percent |
| Reported | Daily average |
| Window | Midnight to midnight, profile local time |
| Wearable required | Yes |
| API | GET /api/v1/profile/biomarker/{externalId}?categories=vitals plus one types parameter per field and a date range |
| Webhook | BiomarkerCreatedIntegrationEvent. Entries update in place, upsert on id. |
Data sources
| Platform | Record | Notes |
|---|---|---|
| iOS, HealthKit | oxygenSaturation | Apple Watch Series 6 and later, background readings mostly during sleep, plus on-demand readings |
| Android, Health Connect | OxygenSaturationRecord | Samsung Health, Fitbit, Garmin Connect, Pixel Watch and other writers, mostly overnight |
| Direct integrations | Provider-dependent | Oura supplies overnight SpO2 where the user has enabled it |
| Phone only | None |
How Sahha calculates oxygen saturation
- Pick one source for the day within the oxygen category, which also covers respiratory rate and VO2 max. One source wins per day; values are never blended.
- Daily is the average of that source’s readings with an end time inside the local day.
- Sleep averages each hour in which the user was in bed for at least 45 minutes, according to sleep records, then averages those hours. Hours before midnight belong to the previous calendar date.
Absence means no readings from the winning source that day.
Interpreting the value
The level is almost always in the high nineties. For healthy users at low altitude the daily average sits in a narrow band, and single readings below it are usually motion or a loose strap rather than physiology.
Change against the user’s own baseline is the signal. A sustained drop in the sleep average over several nights, with no change in altitude, is worth surfacing gently. It is also the pattern most manufacturers use to prompt users about sleep-disordered breathing, and it is not a diagnosis.
Altitude moves it immediately. A user who travels to a mountain town will show a lower average for the whole stay.
Related scores and insights
Oxygen saturation is not a score factor and has no trend or comparison insight.
Use cases
A sleep-time baseline with a soft alert. Keep a 14-night rolling average of the sleep value and surface a sustained drop as information to raise with a clinician, in language that avoids diagnosis.
GET /api/v1/profile/biomarker/{externalId}?categories=vitals&types=oxygen_saturation_sleep&startDateTime=2026-08-19&endDateTime=2026-09-02Travel context. Pair a drop with a change in the profile’s time zone offset to explain altitude and travel rather than alarm the user.
Limitations
- Spot readings, averaged. A day’s value can rest on a handful of readings, and Apple Watch takes them opportunistically.
- Accuracy varies with fit and skin. Loose straps, tattoos, cold hands, and movement all lower readings, and pulse oximetry reads less accurately on darker skin [1].
- Not for medical use. Do not build alerts that imply diagnosis.
- Absence is not zero.
Related guides
- Respiratory rate for the companion overnight signal
- Heart rate for sleeping heart rate from the same nights
- Data dictionary in the developer docs
References
- Sjoding MW, Dickson RP, Iwashyna TJ, Gay SE, Valley TS. Racial bias in pulse oximetry measurement. New England Journal of Medicine. 2020;383(25):2477-2478. https://doi.org/10.1056/NEJMc2029240