Skip to main content

Mobile Native: Apple HealthKit & Health Connect

For applications with an iOS or Android client, the native operating system health hubs (Apple HealthKit on iOS and Google Health Connect on Android) provide a unified local store for wearable data.


1. Required Health Permissions​

Your mobile application must request read access for the following record types:

MetricApple HealthKit IdentifierGoogle Health Connect RecordSensia Payload Field
Heart RateHKQuantityTypeIdentifierHeartRateHeartRateRecordHR_day_1h
Respiratory RateHKQuantityTypeIdentifierRespiratoryRateRespiratoryRateRecordRR_day_1h
Sleep AnalysisHKCategoryTypeIdentifierSleepAnalysisSleepSessionRecord & SleepStageRecordsleepTimeSeconds, deepSleepSeconds, etc.
Date of Birth / AgeHKCharacteristicTypeIdentifierDateOfBirthStored in client user profileage
Biological SexHKCharacteristicTypeIdentifierBiologicalSexStored in client user profilesex
Height & WeightHKQuantityTypeIdentifierHeight, ...BodyMassHeightRecord, WeightRecordheight_cm, weight_kg

2. Transformation Recipe (TypeScript / Node.js)​

Below is a reference TypeScript function demonstrating how to transform queried HealthKit/Health Connect records into the exact current_session payload expected by Sensia:

sensia-mobile-payload-builder.ts
export interface RawSleepInterval {
stage: "deep" | "rem" | "light" | "core" | "awake";
startDate: string; // ISO string
endDate: string; // ISO string
}

export interface UserDemographics {
age?: number;
height_cm?: number;
weight_kg?: number;
sex?: "M" | "F";
}

export interface MobileBiometricsInput {
hrDaySamples: number[]; // Heart rate readings over past 1h (BPM)
rrDaySamples?: number[]; // Respiratory rate readings over past 1h (br/min)
hrNightSamples: number[]; // Heart rate readings during sleep (BPM)
rrNightSamples?: number[]; // Respiratory rate readings during sleep (br/min)
sleepIntervals: RawSleepInterval[];
demographics: UserDemographics;
}

export function buildSensiaCurrentSession(input: MobileBiometricsInput) {
let deepSeconds = 0;
let remSeconds = 0;
let lightSeconds = 0;
let awakeSeconds = 0;

for (const interval of input.sleepIntervals) {
const durationSeconds = Math.max(
0,
(new Date(interval.endDate).getTime() - new Date(interval.startDate).getTime()) / 1000
);

switch (interval.stage) {
case "deep":
deepSeconds += durationSeconds;
break;
case "rem":
remSeconds += durationSeconds;
break;
case "light":
case "core": // Apple HealthKit 'asleepCore' maps to light sleep
lightSeconds += durationSeconds;
break;
case "awake":
awakeSeconds += durationSeconds;
break;
}
}

// Total actual sleep time (excluding awake intervals)
const sleepTimeSeconds = deepSeconds + remSeconds + lightSeconds;

return {
current_session: {
HR_day_1h: input.hrDaySamples,
RR_day_1h: input.rrDaySamples ?? [],
HR_night: input.hrNightSamples,
RR_night: input.rrNightSamples ?? [],
sleepTimeSeconds: Math.round(sleepTimeSeconds),
deepSleepSeconds: Math.round(deepSeconds),
remSleepSeconds: Math.round(remSeconds),
lightSleepSeconds: Math.round(lightSeconds),
awakeSleepSeconds: Math.round(awakeSeconds),
deepPercentage: sleepTimeSeconds > 0 ? Number((deepSeconds / sleepTimeSeconds).toFixed(4)) : 0,
remPercentage: sleepTimeSeconds > 0 ? Number((remSeconds / sleepTimeSeconds).toFixed(4)) : 0,
lightPercentage: sleepTimeSeconds > 0 ? Number((lightSeconds / sleepTimeSeconds).toFixed(4)) : 0,
age: input.demographics.age,
height_cm: input.demographics.height_cm,
weight_kg: input.demographics.weight_kg,
sex: input.demographics.sex ?? null,
},
};
}

3. Transformation Recipe (Python)​

If your mobile app sends the raw records to your backend service for processing before calling Sensia:

sensia_payload_builder.py
from datetime import datetime
from typing import List, Dict, Any, Optional

def build_sensia_session(
hr_day: List[float],
hr_night: List[float],
sleep_stages: List[Dict[str, Any]],
rr_day: Optional[List[float]] = None,
rr_night: Optional[List[float]] = None,
demographics: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Transforms mobile health samples into the Sensia current_session payload.
"""
deep_sec = 0.0
rem_sec = 0.0
light_sec = 0.0
awake_sec = 0.0

for stage in sleep_stages:
start = datetime.fromisoformat(stage["startDate"])
end = datetime.fromisoformat(stage["endDate"])
dur = max(0.0, (end - start).total_seconds())

kind = stage["stage"].lower()
if kind == "deep":
deep_sec += dur
elif kind == "rem":
rem_sec += dur
elif kind in ("light", "core"):
light_sec += dur
elif kind == "awake":
awake_sec += dur

total_sleep_sec = deep_sec + rem_sec + light_sec
demo = demographics or {}

return {
"current_session": {
"HR_day_1h": [float(x) for x in hr_day],
"RR_day_1h": [float(x) for x in (rr_day or [])],
"HR_night": [float(x) for x in hr_night],
"RR_night": [float(x) for x in (rr_night or [])],
"sleepTimeSeconds": int(round(total_sleep_sec)),
"deepSleepSeconds": int(round(deep_sec)),
"remSleepSeconds": int(round(rem_sec)),
"lightSleepSeconds": int(round(light_sec)),
"awakeSleepSeconds": int(round(awake_sec)),
"deepPercentage": round(deep_sec / total_sleep_sec, 4) if total_sleep_sec > 0 else 0.0,
"remPercentage": round(rem_sec / total_sleep_sec, 4) if total_sleep_sec > 0 else 0.0,
"lightPercentage": round(light_sec / total_sleep_sec, 4) if total_sleep_sec > 0 else 0.0,
"age": demo.get("age"),
"height_cm": demo.get("height_cm"),
"weight_kg": demo.get("weight_kg"),
"sex": demo.get("sex"),
}
}

Next Steps​