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:
| Metric | Apple HealthKit Identifier | Google Health Connect Record | Sensia Payload Field |
|---|---|---|---|
| Heart Rate | HKQuantityTypeIdentifierHeartRate | HeartRateRecord | HR_day_1h |
| Respiratory Rate | HKQuantityTypeIdentifierRespiratoryRate | RespiratoryRateRecord | RR_day_1h |
| Sleep Analysis | HKCategoryTypeIdentifierSleepAnalysis | SleepSessionRecord & SleepStageRecord | sleepTimeSeconds, deepSleepSeconds, etc. |
| Date of Birth / Age | HKCharacteristicTypeIdentifierDateOfBirth | Stored in client user profile | age |
| Biological Sex | HKCharacteristicTypeIdentifierBiologicalSex | Stored in client user profile | sex |
| Height & Weight | HKQuantityTypeIdentifierHeight, ...BodyMass | HeightRecord, WeightRecord | height_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"),
}
}