Skip to content

Repository files navigation

capacitor-health

Capacitor plugin to query data from Apple Health and Google Health Connect

Thanks and attribution

Some parts, concepts and ideas are borrowed from cordova-plugin-health. Big thanks to @dariosalvi78 for the support.

Install

npm install capacitor-health
npx cap sync

Setup

iOS

  • Make sure your app id has the 'HealthKit' entitlement when this plugin is installed (see iOS dev center).
  • Also, make sure your app and App Store description comply with the Apple review guidelines.
  • There are two keys to be added to the info.plist file: NSHealthShareUsageDescription and NSHealthUpdateUsageDescription.

Android

  • Android Manifest in application tag
        <!-- For supported versions through Android 13, create an activity to show the rationale
    of Health Connect permissions once users click the privacy policy link. -->
        <activity
            android:name="com.fit_up.health.capacitor.PermissionsRationaleActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
            </intent-filter>
        </activity>

        <!-- For versions starting Android 14, create an activity alias to show the rationale
         of Health Connect permissions once users click the privacy policy link. -->
        <activity-alias
            android:name="ViewPermissionUsageActivity"
            android:exported="true"
            android:targetActivity="com.fit_up.health.capacitor.PermissionsRationaleActivity"
            android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
            <intent-filter>
                <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
                <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
            </intent-filter>
        </activity-alias>
  • Android Manifest in root tag
    <queries>
        <package android:name="com.google.android.apps.healthdata" />
    </queries>
    
    <uses-permission android:name="android.permission.health.READ_STEPS" />
    <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
    <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED" />
    <uses-permission android:name="android.permission.health.READ_DISTANCE" />
    <uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED" />
    <uses-permission android:name="android.permission.health.READ_EXERCISE" />
    <uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTE" />
    <uses-permission android:name="android.permission.health.READ_HEART_RATE" />
    <uses-permission android:name="android.permission.health.READ_WEIGHT" />
    <uses-permission android:name="android.permission.health.READ_HEIGHT" />
    <uses-permission android:name="android.permission.health.READ_BODY_FAT" />
    <uses-permission android:name="android.permission.health.READ_LEAN_BODY_MASS" />
    <uses-permission android:name="android.permission.health.READ_NUTRITION" />

Only declare the permissions your app actually requests - Health Connect shows every declared permission on the consent screen.

Flights of stairs

flights-climbed is available through both queryAggregated and queryRecords. Apple Health calls these flights climbed and Health Connect calls them floors climbed, but both define one as roughly three meters of elevation gain, so the plugin exposes a single type with one permission and one unit.

dataType Permission Unit Apple Health Health Connect
flights-climbed READ_FLIGHTS_CLIMBED count flightsClimbed FloorsClimbedRecord
await Health.requestHealthPermissions({ permissions: ['READ_FLIGHTS_CLIMBED'] });

// Aggregated - one total per bucket
const { aggregatedData } = await Health.queryAggregated({
  startDate: '2026-01-01T00:00:00.000Z',
  endDate: '2026-01-08T00:00:00.000Z',
  dataType: 'flights-climbed',
  bucket: 'day',
});
// [{ startDate: '...', endDate: '...', value: 14 }]

// Raw - every record, with the app that wrote it
const { records } = await Health.queryRecords({
  startDate: '2026-01-01T00:00:00.000Z',
  endDate: '2026-01-08T00:00:00.000Z',
  dataType: 'flights-climbed',
});
// [{ startDate: '...', endDate: '...', value: 3, sourceBundleId: '...', sourceName: '...', manual: false }]

Notes:

  • Health Connect can report fractional flights, Apple Health always reports whole ones.
  • Unlike body composition, these records cover a time span, so startDate and endDate differ.
  • The Android permission is android.permission.health.READ_FLOORS_CLIMBED - Health Connect names it after floors, while the plugin's cross-platform permission is READ_FLIGHTS_CLIMBED.
  • queryAggregated only supports bucket: 'day' on Android; 'hour' and 'week' work on iOS only. That limit applies to every aggregated data type, not just this one.

Body composition

queryRecords reads body composition as individual measurements. The four supported types are available on both platforms and behave identically - same permission name, same unit, same value range - so callers do not need to branch on the platform.

dataType Permission Unit Apple Health Health Connect
weight READ_WEIGHT kilograms bodyMass WeightRecord
height READ_HEIGHT meters height HeightRecord
body-fat READ_BODY_FAT percent (0 - 100) bodyFatPercentage BodyFatRecord
lean-body-mass READ_LEAN_BODY_MASS kilograms leanBodyMass LeanBodyMassRecord
await Health.requestHealthPermissions({ permissions: ['READ_WEIGHT', 'READ_BODY_FAT'] });

const { records } = await Health.queryRecords({
  startDate: '2026-01-01T00:00:00.000Z',
  endDate: '2026-02-01T00:00:00.000Z',
  dataType: 'weight',
});
// [{ startDate: '...', endDate: '...', value: 81.4, sourceBundleId: '...', sourceName: '...', manual: false }]

Notes:

  • These are point-in-time measurements, so startDate and endDate of each record are equal.
  • Apple Health stores body fat as a fraction (0 - 1); the plugin scales it to 0 - 100 to match Health Connect.
  • queryAggregated does not support these types on either platform. They are discrete measurements, and summing them is meaningless - four of the underlying Health Connect records do not define an aggregate metric at all.
  • Bone mass and body water exist only in Health Connect; BMI and waist circumference exist only in Apple Health. None of them are exposed, to keep the API platform-independent.
  • iOS dates must include fractional seconds (2026-01-01T00:00:00.000Z).

Nutrition

queryNutrition reads logged food and meals, with energy and macronutrients. It needs the READ_NUTRITION permission.

await Health.requestHealthPermissions({ permissions: ['READ_NUTRITION'] });

const { entries } = await Health.queryNutrition({
  startDate: '2026-01-01T00:00:00.000Z',
  endDate: '2026-01-02T00:00:00.000Z',
  limit: 100, // optional
  includeUngroupedSamples: true, // optional, iOS only
});
// [{
//   id: '...', startDate: '...', endDate: '...',
//   name: 'Porridge', mealType: 'breakfast',
//   energyKcal: 310,
//   macros: { proteinG: 11, carbohydratesG: 54, fatG: 6 },
//   sourceBundleId: 'com.example.food', sourceName: 'Food App',
//   grouping: 'record',
// }]

The result shape is the same on both platforms:

  • Energy is always kilocalories and macros are always grams, whatever unit the writing app used.
  • A nutrient the writing app did not record is left out, never reported as 0. A recorded 0 is kept.
  • Entries are sorted by startDate ascending. limit returns the earliest limit entries.

The platforms store food differently, though, so some fields depend on the platform:

Android (Health Connect) iOS (HealthKit)
One entry is one NutritionRecord one food correlation (HKCorrelationType(.food)), plus loose samples with includeUngroupedSamples
grouping 'record' 'correlation', or 'sample' for loose samples
Nutrient values read from the record summed over the correlation's member samples
name NutritionRecord.name HKMetadataKeyFoodType metadata
mealType always set; MEAL_TYPE_UNKNOWN becomes 'unknown' usually undefined - HealthKit has no standard meal type (see below)
sourceName omitted - Health Connect records carry no app name name of the writing app
includeUngroupedSamples ignored see below
Permission android.permission.health.READ_NUTRITION read access to dietary energy, protein, carbohydrates, total fat, saturated fat, sugar and fiber

Notes:

  • iOS loose samples. Many apps write single nutrient samples (for example just the energy) without wrapping them in a food correlation. These are not returned by default. With includeUngroupedSamples: true, each sample that is not a member of a returned correlation is added as its own entry with grouping: 'sample' and only that one nutrient set. Samples are never merged into meals heuristically, so one meal can show up as several sample entries.
  • iOS meal type. HealthKit defines no meal type. The plugin only sets mealType when the correlation's (or sample's) metadata has a key named Meal, MealType, meal_type or HKFoodMeal (case-insensitive) with the value breakfast, lunch, dinner, snack or snacks (case-insensitive). Anything else leaves it undefined. The meal type is never guessed from the time of day.
  • Android history. Like every other query in this plugin, queryNutrition does not request READ_HEALTH_DATA_HISTORY, so Health Connect only returns data from 30 days before the permission was first granted.
  • iOS dates must include fractional seconds (2026-01-01T00:00:00.000Z).

API

isHealthAvailable()

isHealthAvailable() => Promise<{ available: boolean; }>

Checks if health API is available. Android: If false is returned, the Google Health Connect app is probably not installed. See showHealthConnectInPlayStore()

Returns: Promise<{ available: boolean; }>


checkHealthPermissions(...)

checkHealthPermissions(permissions: PermissionsRequest) => Promise<PermissionResponse>

Android only: Returns for each given permission, if it was granted by the underlying health API

Param Type Description
permissions PermissionsRequest permissions to query

Returns: Promise<PermissionResponse>


requestHealthPermissions(...)

requestHealthPermissions(permissions: PermissionsRequest) => Promise<PermissionResponse>

Requests the permissions from the user.

Android: Apps can ask only a few times for permissions, after that the user has to grant them manually in the Health Connect app. See openHealthConnectSettings()

iOS: If the permissions are already granted or denied, this method will just return without asking the user. In iOS we can't really detect if a user granted or denied a permission. The return value reflects the assumption that all permissions were granted.

Param Type Description
permissions PermissionsRequest permissions to request

Returns: Promise<PermissionResponse>


openAppleHealthSettings()

openAppleHealthSettings() => Promise<void>

Opens the apps settings, which is kind of wrong, because health permissions are configured under: Settings > Apps > (Apple) Health > Access and Devices > [app-name] But we can't go there directly.


openHealthConnectSettings()

openHealthConnectSettings() => Promise<void>

Opens the Google Health Connect app


showHealthConnectInPlayStore()

showHealthConnectInPlayStore() => Promise<void>

Opens the Google Health Connect app in PlayStore


queryAggregated(...)

queryAggregated(request: QueryAggregatedRequest) => Promise<QueryAggregatedResponse>

Query aggregated data

flights-climbed behaves identically on Android and iOS - one flight is roughly three meters of elevation gain on both platforms.

Param Type
request QueryAggregatedRequest

Returns: Promise<QueryAggregatedResponse>


queryWorkouts(...)

queryWorkouts(request: QueryWorkoutRequest) => Promise<QueryWorkoutResponse>

Query workouts

Param Type
request QueryWorkoutRequest

Returns: Promise<QueryWorkoutResponse>


queryRecords(...)

queryRecords(request: QueryRecordsRequest) => Promise<QueryRecordsResponse>

Query individual records for a given data type. Unlike queryAggregated, this returns each record separately with its data origin, which is useful for detecting duplicate sources.

Supports steps, flights-climbed and the body composition types weight, height, body-fat and lean-body-mass. All of them behave identically on Android and iOS - see {@link RecordDataType} for the units.

Body composition measurements are taken at a single point in time, so startDate and endDate of the returned records are equal.

Param Type
request QueryRecordsRequest

Returns: Promise<QueryRecordsResponse>


queryNutrition(...)

queryNutrition(request: QueryNutritionRequest) => Promise<QueryNutritionResponse>

Query logged food and meals, with energy and macronutrients.

Requires the READ_NUTRITION permission. Entries are sorted by startDate ascending. Energy is always in kilocalories and macros in grams. A nutrient the source did not record is omitted, never reported as 0.

The platforms store food differently, which shows up in the entry's grouping:

  • Android (Health Connect): every NutritionRecord is one entry with grouping: 'record'. Health Connect stores the meal type natively.
  • iOS (HealthKit): every food correlation (HKCorrelationType(.food)) is one entry with grouping: 'correlation'; its nutrients are summed from the correlation's member samples. HealthKit has no standard meal type, so mealType is usually undefined on iOS.
  • iOS with includeUngroupedSamples: true: nutrient samples that are not part of any returned food correlation are added as one entry each with grouping: 'sample' and only that one nutrient set. Many apps write loose samples instead of correlations, so without this option their data is not returned on iOS.
Param Type
request QueryNutritionRequest

Returns: Promise<QueryNutritionResponse>


Interfaces

PermissionResponse

Prop Type
permissions { [key: string]: boolean; }[]

PermissionsRequest

Prop Type
permissions HealthPermission[]

QueryAggregatedResponse

Prop Type
aggregatedData AggregatedSample[]

AggregatedSample

Prop Type
startDate string
endDate string
value number

QueryAggregatedRequest

Prop Type Description
startDate string
endDate string
dataType 'steps' | 'active-calories' | 'flights-climbed' | 'mindfulness'
bucket string
dataOrigins string[] Optional list of package names (Android) or bundle identifiers (iOS) to restrict the aggregation to. When omitted or empty, data from all sources is included. Example: ['com.sec.android.app.shealth'] to only aggregate Samsung Health data.

QueryWorkoutResponse

Prop Type
workouts Workout[]

Workout

Prop Type
startDate string
endDate string
workoutType string
sourceName string
id string
duration number
distance number
steps number
calories number
sourceBundleId string
route RouteSample[]
heartRate HeartRateSample[]

RouteSample

Prop Type
timestamp string
lat number
lng number
alt number

HeartRateSample

Prop Type
timestamp string
bpm number

QueryWorkoutRequest

Prop Type
startDate string
endDate string
includeHeartRate boolean
includeRoute boolean
includeSteps boolean

QueryRecordsResponse

Prop Type
records HealthRecord[]

HealthRecord

Prop Type
startDate string
endDate string
value number
sourceBundleId string
sourceName string
manual boolean

QueryRecordsRequest

Prop Type
startDate string
endDate string
dataType RecordDataType

QueryNutritionResponse

Prop Type
entries NutritionEntry[]

NutritionEntry

Prop Type Description
id string Health Connect record id (Android) or HealthKit object UUID (iOS).
startDate string
endDate string
name string Food or meal name, if the source stored one.
mealType MealType
energyKcal number Energy in kilocalories. Omitted if the source did not record it.
macros NutritionMacros
sourceBundleId string Package name (Android) or bundle identifier (iOS) of the writing app.
sourceName string Name of the writing app. iOS only - Health Connect records carry no app name, so this is omitted on Android.
grouping NutritionGrouping

NutritionMacros

Macronutrients of a nutrition entry, in grams. A nutrient the source did not record is omitted, never 0.

Prop Type
proteinG number
carbohydratesG number
fatG number
saturatedFatG number
sugarG number
fiberG number

QueryNutritionRequest

Prop Type Description
startDate string
endDate string
limit number Maximum number of entries to return: the earliest limit entries after sorting. When omitted, all entries in the range are returned.
includeUngroupedSamples boolean iOS only; ignored on Android, where every entry is a record. When true, nutrient samples that belong to no returned food correlation are returned as individual entries with grouping: 'sample'. Defaults to false.

Type Aliases

HealthPermission

'READ_STEPS' | 'READ_WORKOUTS' | 'WRITE_WORKOUTS' | 'READ_ACTIVE_CALORIES' | 'READ_TOTAL_CALORIES' | 'READ_DISTANCE' | 'READ_FLIGHTS_CLIMBED' | 'READ_HEART_RATE' | 'READ_ROUTE' | 'READ_MINDFULNESS' | 'READ_WEIGHT' | 'READ_HEIGHT' | 'READ_BODY_FAT' | 'READ_LEAN_BODY_MASS' | 'READ_NUTRITION'

RecordDataType

Data types that can be read as individual records via queryRecords.

All of these behave identically on Android and iOS: same units, same value ranges, same result shape.

dataType Permission Unit
steps READ_STEPS count
flights-climbed READ_FLIGHTS_CLIMBED count
weight READ_WEIGHT kilograms
height READ_HEIGHT meters
body-fat READ_BODY_FAT percent (0 - 100)
lean-body-mass READ_LEAN_BODY_MASS kilograms

flights-climbed counts flights of stairs. Both platforms define one flight as roughly three meters of elevation gain (Apple flightsClimbed, Health Connect FloorsClimbedRecord). Health Connect can report fractional flights, Apple Health always reports whole ones.

'steps' | 'flights-climbed' | 'weight' | 'height' | 'body-fat' | 'lean-body-mass'

MealType

Meal a nutrition entry belongs to.

Android maps Health Connect's MealType constants; MEAL_TYPE_UNKNOWN and unrecognised values become 'unknown'. iOS has no standard meal type metadata, so it only sets this when the writing app stored a recognisable meal hint (see README).

'breakfast' | 'lunch' | 'dinner' | 'snack' | 'unknown'

NutritionGrouping

How a nutrition entry was assembled from the platform's data.

  • record: one Health Connect NutritionRecord (Android).
  • correlation: one HealthKit food correlation, nutrients summed over its member samples (iOS).
  • sample: one loose HealthKit nutrient sample that belongs to no returned correlation (iOS, only with includeUngroupedSamples).

'record' | 'correlation' | 'sample'

About

Capacitor plugin to read data from Apple Health and Google Health Connect

Topics

Resources

Contributing

Stars

20 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages