mirobody/kernel/decoders/apple.py, so a
type name means the same thing whichever way the data arrives.
The vocabulary is HealthKit’s own
type is a HealthKit identifier: HKQuantityTypeIdentifierHeartRate, not
HEART_RATE. That is what export.xml carries, what HealthKit names a type on
the device, and what this endpoint now accepts.
Earlier releases took a second vocabulary here, a FlutterHealthTypeEnum of
names like HEART_RATE, and mapped it onto the same catalogue with a 74-row
table. It described one client that no longer exists, and two tables for one
mapping is how they drift. If you are moving a client off it: send Apple’s
identifier, startDate/endDate instead of dateFrom/dateTo, a plain
value instead of {"numericValue": n}, and unit instead of unitSymbol.
POST /apple/health
Accepts gzip (Content-Encoding: gzip).
startDate/endDate take epoch milliseconds or
Apple’s own date string (2026-06-01 08:00:00 +0800); endDate defaults to
startDate. A record with no parsable start decodes to nothing, never to a
guessed time.
unit is per record, not per type. Apple writes it that way (it is CDATA
in their DTD and follows the device’s region), so one upload can carry mg/dL
and mmol/L for the same type. Every value is converted by reading its own
unit. Send what Apple gave you and do not pre-convert.
Percent-typed quantities are fractions. HKUnit.percent() ranges 0.0 to
1.0, so an oxygen saturation of 98% is 0.98.
Category records
A category record’svalue is the HKCategoryValue* name as a string, kept
verbatim:
endDate - startDate in milliseconds. The four
stages that are time asleep (Deep, Core, REM, Unspecified) each also land as a
sleepAnalysis_Asleep(Total) record. InBed and Awake do not.
Blood pressure
Apple writes a cuff reading as two separate records, and so should you. They are recognised as one measurement when they share astartDate and a
sourceName. A HKCorrelationTypeIdentifierBloodPressure record carrying
systolic and diastolic is also accepted.
What the endpoint accepts
47 identifiers: 35 quantity types, 10 category types, sleep analysis and the blood pressure correlation. The tables inmirobody/kernel/decoders/apple.py are
the only authoritative list, and they are one command away, so nothing here can
drift into being a second wrong copy:
Adding a data type
One table, one file. Add the identifier toQUANTITY (or CATEGORY) in
mirobody/kernel/decoders/apple.py,
pointing at a catalogue metric. If that metric does not exist yet, add it to
StandardIndicator in
mirobody/translate/indicators_info.py
with its canonical unit, and see that package’s
README.
Add a case to mirobody/kernel/decoders/samples/apple/ in the same change: the
expected numbers there are worked by hand, never read back from the decoder,
which is what makes them evidence.
Response
Notes
- Duplicates are the client’s problem: deduplicate before sending.
- Volume: large uploads are fine, and gzip is worth using.
- Time: send the offset. A record’s own
timezonewins overmetaInfo.