Skip to content
Shiny.NET

Health

NuGet package Shiny.AppDeviceBridge.Health

The health bridge reads and writes HealthKit and Health Connect data: permissions, bucketed reads, writes and live readings, backed by Shiny.Health.

Terminal window
dotnet add package Shiny.AppDeviceBridge.Health
builder.UseAppDeviceBridge(
bridge => bridge.AddHealthBridge(),
webApp => { /* … */ }
);
Client C# TypeScript
IHealthBridge AddHealthBridgeClient() HealthBridge
Topic Details
Platforms HealthKit on iOS, Health Connect on Android. Other platforms return 501. On Android without Health Connect, every call except GET health returns 503.
Access ask for access before reading, one entry per type. On iOS, HealthKit never reveals a read denial: granted can be true and reads still come back empty. On Android, reading a type that wasn’t granted returns 403.
Types GET health lists every type, with its unit and whether it’s bucketed.
Reads numeric types and blood pressure are totalled or averaged into minutes, hours or days buckets. Cycle tracking, workouts and nutrition come back as individual records. A read covers at most 366 days and 2,000 buckets.
Writes POST health/samples/{type} takes start, end and the fields for the type: value, systolic/diastolic, flow, workout and so on.
Listeners a listener sends health.reading for new samples, so listen to that first; without a listener it answers 409. At most 8 run at once, and they stop once nothing listens to health.reading.
Privacy health values are never logged.
Platform setup on iOS, add the HealthKit entitlement plus NSHealthShareUsageDescription and NSHealthUpdateUsageDescription. On Android, set minSdk 26, add a android.permission.health.* permission for each record type, the Health Connect <queries> entry, and the VIEW_PERMISSION_USAGE activity-alias. MainActivity also needs a filter for androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE.
const health = new HealthBridge();
await health.requestAccess({ permissions: [{ type: "StepCount", access: "Read" }] });
const today = new Date(); today.setHours(0, 0, 0, 0);
const steps = await health.getSamples("StepCount", today, new Date(), { interval: "Hours" });
GET/_bridge/health#

Whether the store is available, every type with its unit, and the types being watched.

C#IHealthBridge.GetStatusAsyncTSHealthBridge.getStatus
Response
200 HealthStatus
HealthStatus · 3 fields
availablebooleanFalse where the store is missing — Health Connect not installed.
typesHealthTypeInfo[]
listenersHealthListener[]The types being watched.
GET/_bridge/health/samples/{type}#

Samples in a range of at most 366 days. Numeric types and blood pressure are bucketed by interval, at most 2,000 buckets; record types — cycle tracking, workouts, nutrition — are individual records and ignore it.

C#IHealthBridge.GetSamplesAsyncTSHealthBridge.getSamples
ParameterInType
typepathHealthDataTyperequired
startqueryDate | stringrequired
endqueryDate | stringrequired
intervalqueryHealthIntervaloptional
Response
200 HealthSamples
HealthSamples · 3 fields
typeHealthDataType
unitstring | nullWhat values are measured in; null for record types.
samplesHealthSample[]
POST/_bridge/health/samples/{type}#

Writes one sample. Which fields it needs depends on the type; a missing one fails with 400.

C#IHealthBridge.WriteSampleAsyncTSHealthBridge.writeSample
ParameterInType
typepathHealthDataTyperequired
Body
application/json · HealthSampleInput
HealthSampleInput · 24 fields
startDate | string
endDate | string | null
valuenumber | null
systolicnumber | null
diastolicnumber | null
flowMenstrualFlow | null
isCycleStartboolean | null
protectionSexualActivityProtection | null
outcomeOvulationTestOutcome | null
appearanceCervicalMucusAppearance | null
workoutWorkoutType | null
totalEnergyKilocaloriesnumber | null
totalDistanceMetersnumber | null
titlestring | null
mealMealType | null
namestring | null
energyKilocaloriesnumber | null
proteinGramsnumber | null
carbohydratesGramsnumber | null
totalFatGramsnumber | null
fiberGramsnumber | null
sugarGramsnumber | null
sodiumGramsnumber | null
cholesterolGramsnumber | null
Response
204 No content
Errors
400Bad request
POST/_bridge/health/listeners/{type}#

Watches a type; readings arrive through onReading, so listen first: without a listener it fails with 409. At most 8 types at once. Every watch stops once onReading has no listener left.

C#IHealthBridge.StartListeningAsyncTSHealthBridge.startListening
ParameterInType
typepathHealthDataTyperequired
Body
application/json · HealthListenerRequest
HealthListenerRequest · 1 field
pollingIntervalSecondsnumber | nullHow often Android polls, 5–300 seconds. iOS is pushed and ignores it.
Response
200 HealthListener
HealthListener · 1 field
typeHealthDataType
Errors
409Conflict
DELETE/_bridge/health/listeners/{type}#

Stops watching a type. Fails with 404 when it is not being watched.

C#IHealthBridge.StopListeningAsyncTSHealthBridge.stopListening
ParameterInType
typepathHealthDataTyperequired
Response
204 No content
Errors
404Not found
EVENThealth.stopped#

Watching a type ended, because it was stopped or it failed.

C#IHealthBridge.OnStoppedAsyncTSHealthBridge.onStopped
Payload
HealthListenerStopped
typeHealthDataType
errorstring | nullWhy it ended, when it failed rather than being stopped.

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

HealthStatusobject#
availablebooleanFalse where the store is missing — Health Connect not installed.
typesHealthTypeInfo[]
listenersHealthListener[]The types being watched.
HealthTypeInfoobject#
typeHealthDataType
kindHealthDataKind
unitstring | nullWhat values are measured in — count, bpm, kg; null for record types.
bucketedbooleanWhether reads are bucketed by interval.
HealthDataTypeenum#
"StepCount""HeartRate""Calories""Distance""Weight""Height""BodyFatPercentage""RestingHeartRate""BloodPressure""OxygenSaturation""SleepDuration""Hydration""MenstruationFlow""BloodGlucose""BodyTemperature""BasalBodyTemperature""RespiratoryRate""Vo2Max""HeartRateVariability""LeanBodyMass""BasalEnergyBurned""ActiveEnergyBurned""FloorsClimbed""WheelchairPushes""Speed""Power""SexualActivity""OvulationTest""CervicalMucus""IntermenstrualBleeding""Workout""Nutrition"
HealthDataKindenum#

How a type is read: bucketed numbers, bucketed blood pressure pairs, or individual records.

"Numeric""BloodPressure""Record"
HealthAccessTypeenum#
"Read""Write""ReadWrite"
HealthAccessResultobject#
typeHealthDataType
grantedbooleanWhether the platform reported success. iOS never says whether read access was granted.
HealthIntervalenum#

The bucket size for numeric and blood pressure reads.

"Minutes""Hours""Days"
HealthSampleobject#

One sample. Which fields are set depends on the type: value for numeric types, systolic and diastolic for blood pressure, and the fields named after each record type for the rest.

startstring
endstring
valuenumber | null
systolicnumber | null
diastolicnumber | null
flowMenstrualFlow | null
isCycleStartboolean | null
protectionSexualActivityProtection | null
outcomeOvulationTestOutcome | null
appearanceCervicalMucusAppearance | null
workoutWorkoutType | null
totalEnergyKilocaloriesnumber | null
totalDistanceMetersnumber | null
titlestring | null
mealMealType | null
namestring | null
energyKilocaloriesnumber | null
proteinGramsnumber | null
carbohydratesGramsnumber | null
totalFatGramsnumber | null
fiberGramsnumber | null
sugarGramsnumber | null
sodiumGramsnumber | null
cholesterolGramsnumber | null
MenstrualFlowenum#
"Unspecified""None""Light""Medium""Heavy"
SexualActivityProtectionenum#
"Unspecified""Protected""Unprotected"
OvulationTestOutcomeenum#
"Inconclusive""Positive""High""Negative"
CervicalMucusAppearanceenum#
"Unspecified""Dry""Sticky""Creamy""Watery""EggWhite"
WorkoutTypeenum#
"Other""Running""Walking""Hiking""Cycling""Swimming""Rowing""Elliptical""StairClimbing""StrengthTraining""HighIntensityIntervalTraining""Yoga""Pilates""Tennis""Basketball""Soccer""Baseball""Golf""Boxing""MartialArts""Dancing"
MealTypeenum#
"Unknown""Breakfast""Lunch""Dinner""Snack"
HealthSampleInputobject#

A sample to write. end defaults to start. Set the fields the type needs: value for numeric types, systolic and diastolic for blood pressure, flow for menstruation flow, outcome for an ovulation test, workout for a workout. Numbers must be finite and not negative; title and name at most 256 characters.

startDate | string
end?Date | string | null
value?number | null
systolic?number | null
diastolic?number | null
flow?MenstrualFlow | null
isCycleStart?boolean | null
protection?SexualActivityProtection | null
outcome?OvulationTestOutcome | null
appearance?CervicalMucusAppearance | null
workout?WorkoutType | null
totalEnergyKilocalories?number | null
totalDistanceMeters?number | null
title?string | null
meal?MealType | null
name?string | null
energyKilocalories?number | null
proteinGrams?number | null
carbohydratesGrams?number | null
totalFatGrams?number | null
fiberGrams?number | null
sugarGrams?number | null
sodiumGrams?number | null
cholesterolGrams?number | null
HealthListenerRequestobject#
pollingIntervalSeconds?number | nullHow often Android polls, 5–300 seconds. iOS is pushed and ignores it.
HealthListenerStoppedobject#
typeHealthDataType
errorstring | nullWhy it ended, when it failed rather than being stopped.

These routes sit under AppDeviceBridgePolicies.Bridges like every other bridge: callers on this device by default. See Security.