@capacitor/health-fitness
Access Android Health Connect and Apple HealthKit data for health and fitness apps. Not available on web.
Installโ
To use npm
npm install @capacitor/health-fitness
To use yarn
yarn add @capacitor/health-fitness
Sync native files
npx cap sync
iOSโ
Your app's Info.plist must declare the following (the plugin cannot set
these on the host app itself):
<key>NSHealthShareUsageDescription</key>
<string>App needs to share health data</string>
<key>NSHealthUpdateUsageDescription</key>
<string>App needs to use health data</string>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.outsystems.health.default</string>
</array>
NSHealthShareUsageDescription/NSHealthUpdateUsageDescription: the messages HealthKit shows the user when requesting read / write access.UIBackgroundModes(fetch,processing) andBGTaskSchedulerPermittedIdentifiers: needed forsetBackgroundJob's background job feature.
The app target also needs the HealthKit capability enabled, with the
following entitlements set to true (both Debug and Release):
com.apple.developer.healthkit: the base entitlement required for any HealthKit access at all.com.apple.developer.healthkit.background-delivery: needed forsetBackgroundJob's background job feature.com.apple.developer.healthkit.recalibrate-estimates: needed to read/write HealthKit's on-device "estimate" variables, e.g.WALKING_SPEEDand VO2 max.
com.apple.developer.healthkit.access should be set to an empty array - it
lists any additional restricted HealthKit types beyond the default set, and
this plugin doesn't use any.
Read about Configuring Info.plist in the iOS Guide for more information on setting iOS permissions in Xcode
Androidโ
This plugin ships a capacitor:sync:after hook
(hooks/capacitorCopyHealthFitnessConfigs.js) that runs on every
npx cap sync and edits your app's generated
android/app/src/main/AndroidManifest.xml to declare the
Health Connect permissions your app actually needs. Health Connect permissions
cannot be requested at runtime the way Capacitor's plugins.<Name> config
values are read - they must exist in the manifest at build time - hence the
sync-time hook instead of getConfig().
Declaring permissionsโ
In your app, create an android/healthfitness.config.json file (next to android/app/):
{
"permissions": {
"HEART_RATE": "Read",
"STEPS": "ReadWrite",
"WEIGHT": "Write",
"HEIGHT": "Read",
"CALORIES_BURNED": "Read",
"SLEEP": "Read",
"BLOOD_PRESSURE": "Read",
"BLOOD_GLUCOSE": "Read",
"BODY_FAT_PERCENTAGE": "Read",
"BASAL_METABOLIC_RATE": "Read",
"WALKING_SPEED": "Read",
"DISTANCE": "Read",
"OXYGEN_SATURATION": "Read",
"BODY_TEMPERATURE": "Read"
},
"groupPermissions": {
"ALL_VARIABLES": "ReadWrite",
"FITNESS_VARIABLES": "Read",
"HEALTH_VARIABLES": "Read",
"PROFILE_VARIABLES": "Read"
}
}
Each value is one of Read, Write, or ReadWrite - this casing is
intentional and distinct from requestHealthPermissions's AccessType
(READ/WRITE/READWRITE), since this file is parsed by the sync-time hook
above, not read by the native plugin at runtime. Both files are optional
per key - only declare what the app actually uses. If neither
healthfitness.config.json nor any key in it is present, the hook falls back
to declaring every Health Connect permission (matching the plugin's
previous, non-configurable behavior) so your app still works even if you
skip configuration, at the cost of declaring more permissions than it needs.
Background jobs and read-history permissionsโ
Two more permission groups are on by default and can be opted out of via
top-level flags in android/healthfitness.config.json:
{
"disableBackgroundJobs": false,
"disableReadHealthDataHistory": false
}
disableBackgroundJobs: trueskipsREAD_HEALTH_DATA_IN_BACKGROUND,POST_NOTIFICATIONS,ACTIVITY_RECOGNITION(both the platform and Google Play Services variants),FOREGROUND_SERVICE,FOREGROUND_SERVICE_HEALTH,HIGH_SAMPLING_RATE_SENSORS, andSCHEDULE_EXACT_ALARM- i.e. everythingsetBackgroundJobneeds.disableReadHealthDataHistory: trueskipsREAD_HEALTH_DATA_HISTORY(lets the app read data older than 30 days before the first Health Connect grant).
Background notification contentโ
setBackgroundJob's foreground notification title/description are read from
your app's res/values/strings.xml (background_notification_title
/ background_notification_description) - and read unconditionally at plugin
load time (app startup), not just when a background job is actually set, so
a missing value crashes the app immediately rather than only when the feature
is used. The hook creates both with sensible defaults if missing, overridable
via android/healthfitness.config.json:
{
"backgroundNotificationTitle": "Health & Fitness",
"backgroundNotificationDescription": "Monitoring your health and fitness data in the background."
}
Privacy policy URLโ
Health Connect requires a privacy policy URL for apps requesting these
permissions - requestHealthPermissions() rejects without one, and Health
Connect opens the URL directly in a browser from its own permissions screen,
so it must be a real, publicly-reachable https:// link, not a bundled local
file. The simplest way to set it is directly in
android/healthfitness.config.json:
{
"privacyPolicyUrl": "https://example.com/privacy-policy"
}
If privacyPolicyUrl isn't set, the hook falls back to deriving one from
capacitor.config.json's server.url + a fixed HealthConnect_PrivacyPolicy.txt
filename - only useful for apps that already serve their web content from a
remote server and host that file there (HealthConnect_PrivacyPolicy.txt in
your app's webDir, copied to android/app/src/main/assets/public/ by
cap sync). Most Capacitor
apps bundle their web assets locally and have no server.url, so this
fallback will never resolve for them - use privacyPolicyUrl directly
instead.
Either way, if strings.xml already has a non-empty privacy_policy_url
(e.g. set by a separate build step), the hook leaves it untouched.
Examplesโ
Every method below is exercised by the plugin's own
example-app -
the snippets here are taken directly from it. Note that every options object
is a plain object whose values are themselves JSON-encoded strings, not typed
fields.
Requesting permissionsโ
Call this before any other method. allVariables/fitnessVariables/
healthVariables/profileVariables/workoutVariables each take a
JSON-encoded { IsActive, AccessType } descriptor (AccessType is READ,
WRITE, or READWRITE) targeting a themed subset of variables:
- Fitness:
STEPS,CALORIES_BURNED,DISTANCE,WALKING_SPEED - Health:
HEART_RATE,SLEEP,BLOOD_PRESSURE,BLOOD_GLUCOSE,OXYGEN_SATURATION,BODY_TEMPERATURE - Profile:
WEIGHT,HEIGHT,BODY_FAT_PERCENTAGE,BASAL_METABOLIC_RATE - Workout (iOS only): HealthKit's workout type, needed for
getWorkoutData()
Setting allVariables's IsActive to true requests every variable at
once (this already includes workout on iOS). Use workoutVariables on its
own to request just workout access without everything else, or
customPermissions to request individual variables directly, e.g.
[{"Variable":"STEPS","AccessType":"READ"}].
import { HealthFitness } from '@capacitor/health-fitness';
await HealthFitness.requestHealthPermissions({
customPermissions: '[]',
allVariables: JSON.stringify({ IsActive: true, AccessType: 'READWRITE' }),
fitnessVariables: JSON.stringify({ IsActive: false, AccessType: 'READWRITE' }),
healthVariables: JSON.stringify({ IsActive: false, AccessType: 'READWRITE' }),
profileVariables: JSON.stringify({ IsActive: false, AccessType: 'READWRITE' }),
workoutVariables: JSON.stringify({ IsActive: false, AccessType: 'READWRITE' }), // allVariables already covers this
});
Querying dataโ
getData() runs an "advanced query": it reads one variable (e.g. STEPS)
over a date range, bucketed into a time unit (e.g. one bucket per day), and
aggregated within each bucket by an operation (e.g. SUM). This example
reads the last 7 days of daily step totals:
import { HealthFitness } from '@capacitor/health-fitness';
// No milliseconds in the date strings - the native date parser only accepts
// "yyyy-MM-dd'T'HH:mm:ssZ", so toISOString()'s fractional-seconds suffix
// must be trimmed off.
const isoDate = (d: Date) => d.toISOString().split('.')[0] + 'Z';
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
const { results } = await HealthFitness.getData({
parameters: JSON.stringify({
Variable: 'STEPS',
StartDate: isoDate(sevenDaysAgo),
EndDate: isoDate(tomorrow),
TimeUnit: 'DAY',
OperationType: 'SUM',
TimeUnitLength: 1,
AdvancedQueryReturnType: 'ALL_DATA',
AdvancedQueryResultType: 'RAW_DATA',
}),
});
// results is itself a JSON-encoded string
console.log(JSON.parse(results ?? '[]'));
Writing dataโ
Android only accepts "profile" variables here (WEIGHT, HEIGHT,
BODY_FAT_PERCENTAGE, BASAL_METABOLIC_RATE); iOS accepts most variables
except category-based ones like SLEEP.
import { HealthFitness } from '@capacitor/health-fitness';
await HealthFitness.writeData({ variable: 'WEIGHT', value: 75 });
Getting the last recorded valueโ
import { HealthFitness } from '@capacitor/health-fitness';
const { results } = await HealthFitness.getLastRecord({ variable: 'STEPS' });
Querying workout data (iOS only)โ
import { HealthFitness } from '@capacitor/health-fitness';
const isoDate = (d: Date) => d.toISOString().split('.')[0] + 'Z';
const { results } = await HealthFitness.getWorkoutData({
parameters: JSON.stringify({
WorkoutTypeVariables: [],
StartDate: isoDate(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)),
EndDate: isoDate(new Date(Date.now() + 24 * 60 * 60 * 1000)),
}),
});
Background jobsโ
A background job watches a single variable and fires a local notification
when a condition is met - for example, notify the user once their daily
step count goes above 100. JobFrequency/TimeUnit control how often the
condition is checked; NotificationFrequency controls how often the
notification itself is allowed to re-fire once the condition is met.
import { HealthFitness } from '@capacitor/health-fitness';
await HealthFitness.setBackgroundJob({
parameters: JSON.stringify({
Variable: 'STEPS',
TimeUnit: 'DAY',
TimeUnitGrouping: 1,
NotificationFrequency: 'DAY',
NotificationFrequencyGrouping: 1,
JobFrequency: 'DAY',
Condition: 'HIGHER',
Value: 100,
NotificationHeader: 'Goal reached!',
NotificationBody: "You've taken over 100 steps today.",
}),
});
List all existing background jobs:
const { jobs } = await HealthFitness.listBackgroundJobs();
const parsedJobs = JSON.parse(jobs);
console.log(parsedJobs);
Update or delete a job using its id:
const jobId = parsedJobs[0].id;
await HealthFitness.updateBackgroundJob({
parameters: JSON.stringify({
Id: jobId,
NotificationFrequency: 'DAY',
NotificationFrequencyGrouping: 1,
Condition: 'HIGHER',
Value: 100,
NotificationHeader: 'Goal reached!',
NotificationBody: "You've taken over 100 steps today.",
IsActive: 'true',
}),
});
await HealthFitness.deleteBackgroundJob({ id: jobId });
Disconnecting / opening Health Connect (Android only)โ
import { HealthFitness } from '@capacitor/health-fitness';
await HealthFitness.disconnectFromHealthConnect();
await HealthFitness.openHealthConnect();
APIโ
requestHealthPermissions(...)โ
requestHealthPermissions(options: RequestHealthPermissionsOptions) => Promise<void>
Requests the given HealthKit / Health Connect permissions.
| Param | Type |
|---|---|
options | |
Since: 1.0.0
getData(...)โ
getData(options: AdvancedQueryOptions) => Promise<AdvancedQueryResult>
Performs an advanced query for a health/fitness variable over a date range.
| Param | Type |
|---|---|
options | |
Returns:
Promise<AdvancedQueryResult>
Since: 1.0.0
getWorkoutData(...)โ
getWorkoutData(options: WorkoutAdvancedQueryOptions) => Promise<WorkoutAdvancedQueryResult>
Performs an advanced query for workout data over a date range.
iOS only - the underlying native Android library has no workout-specific
query. Not implemented on Android, so calling it there rejects with
Capacitor's standard UNIMPLEMENTED error.
| Param | Type |
|---|---|
options | |
Returns:
Promise<WorkoutAdvancedQueryResult>
Since: 1.0.0
writeData(...)โ
writeData(options: WriteDataOptions) => Promise<void>
Writes a value to a health/fitness variable.
| Param | Type |
|---|---|
options | |
Since: 1.0.0
getLastRecord(...)โ
getLastRecord(options: GetLastRecordOptions) => Promise<AdvancedQueryResult>
Retrieves the last recorded value for a variable.
| Param | Type |
|---|---|
options | |
Returns:
Promise<AdvancedQueryResult>
Since: 1.0.0
setBackgroundJob(...)โ
setBackgroundJob(options: SetBackgroundJobOptions) => Promise<void>
Creates a background job that monitors a variable and notifies on a condition.
| Param | Type |
|---|---|
options | |
Since: 1.0.0
deleteBackgroundJob(...)โ
deleteBackgroundJob(options: DeleteBackgroundJobOptions) => Promise<void>
Deletes a background job by id.
| Param | Type |
|---|---|
options | |
Since: 1.0.0
listBackgroundJobs()โ
listBackgroundJobs() => Promise<ListBackgroundJobsResult>
Lists all existing background jobs.
Returns:
Promise<ListBackgroundJobsResult>
Since: 1.0.0
updateBackgroundJob(...)โ
updateBackgroundJob(options: UpdateBackgroundJobOptions) => Promise<void>
Updates an existing background job's parameters.
| Param | Type |
|---|---|
options | |
Since: 1.0.0
disconnectFromHealthConnect()โ
disconnectFromHealthConnect() => Promise<void>
Revokes all Health Connect permissions previously granted to the app.
Android only - HealthKit has no equivalent API for an app to revoke its own access.
Since: 1.0.0
openHealthConnect()โ
openHealthConnect() => Promise<void>
Opens the Health Connect app. Rejects if Health Connect is not installed.
Android only - HealthKit has no equivalent standalone app to open.
Since: 1.0.0
Interfacesโ
RequestHealthPermissionsOptionsโ
| Prop | Type | Description |
|---|---|---|
customPermissions | string | JSON-encoded string: an array of individual variable permission descriptors, e.g. [{"Variable":"STEPS","AccessType":"READ"}]. Use this to request permission for specific variables not covered by (or instead of) the broader groups below. AccessType is READ, WRITE, or READWRITE. |
allVariables | string | JSON-encoded string: {"IsActive": boolean, "AccessType": "READ" | "WRITE" | "READWRITE"}. When IsActive is true, requests the given access to every health/fitness variable the plugin supports. |
fitnessVariables | string | JSON-encoded string: {"IsActive": boolean, "AccessType": "READ" | "WRITE" | "READWRITE"}. Covers the "fitness" variable group: STEPS, CALORIES_BURNED, DISTANCE, WALKING_SPEED. |
healthVariables | string | JSON-encoded string: {"IsActive": boolean, "AccessType": "READ" | "WRITE" | "READWRITE"}. Covers the "health" variable group: HEART_RATE, SLEEP, BLOOD_PRESSURE, BLOOD_GLUCOSE, OXYGEN_SATURATION, BODY_TEMPERATURE (iOS also includes dietary water and dietary energy consumed, which have no Android equivalent). |
profileVariables | string | JSON-encoded string: {"IsActive": boolean, "AccessType": "READ" | "WRITE" | "READWRITE"}. Covers the "profile" variable group: WEIGHT, HEIGHT, BODY_FAT_PERCENTAGE, BASAL_METABOLIC_RATE. |
workoutVariables | string | JSON-encoded string: {"IsActive": boolean, "AccessType": "READ" | "WRITE" | "READWRITE"}. Requests permission for HealthKit's workout type, needed for getWorkoutData(). iOS only - not supported on Android (getWorkoutData() is iOS only; this field is never read there). |
AdvancedQueryResultโ
| Prop | Type | Description |
|---|---|---|
results | string | JSON-encoded string containing the raw result blocks. |
resultDataPoints | string | JSON-encoded string containing chart-ready accelerator data points. |
warning | { code: string; message: string; } | Present only on Android, and only when getData()'s TimeUnit parameter is MILLISECONDS or SECONDS - both are deprecated on Health Connect, so the query silently runs with TimeUnit: 'MINUTE' instead. code is OS-PLUG-HLFT-0405. |
AdvancedQueryOptionsโ
| Prop | Type | Description |
|---|---|---|
parameters | string | JSON-encoded string containing the full query parameters object (variable, startDate, endDate, timeUnit, operationType, timeUnitLength, advancedQueryReturnType, advancedQueryResultType) - a single serialized blob, not individual fields. |
WorkoutAdvancedQueryResultโ
| Prop | Type | Description |
|---|---|---|
results | string | JSON-encoded string containing the raw result blocks. |
WorkoutAdvancedQueryOptionsโ
| Prop | Type | Description |
|---|---|---|
parameters | string | JSON-encoded string containing the full workout query parameters object (workoutTypeVariables, startDate, endDate) - a single serialized blob. |
WriteDataOptionsโ
| Prop | Type | Description |
|---|---|---|
variable | string | The health/fitness variable to write to. Android only accepts "profile" variables - WEIGHT, HEIGHT, BODY_FAT_PERCENTAGE, BASAL_METABOLIC_RATE - any other variable rejects with a "not a profile variable" error. iOS accepts most variables (anything HealthKit represents as a quantity, which includes the profile variables plus most fitness/health ones), except category-based ones like SLEEP. |
value | number | The value to write, in the variable's native unit (e.g. kg for WEIGHT). |
GetLastRecordOptionsโ
| Prop | Type | Description |
|---|---|---|
variable | string | The health/fitness variable to read, e.g. STEPS. |
SetBackgroundJobOptionsโ
| Prop | Type | Description |
|---|---|---|
parameters | string | JSON-encoded string containing the full background job parameters object (variable, timeUnit, timeUnitGrouping, notificationFrequency, notificationFrequencyGrouping, jobFrequency, condition, value, notificationHeader, notificationBody) - a single serialized blob. |
DeleteBackgroundJobOptionsโ
| Prop | Type | Description |
|---|---|---|
id | string | The background job's id, from listBackgroundJobs(). |
ListBackgroundJobsResultโ
| Prop | Type | Description |
|---|---|---|
jobs | string | JSON-encoded string containing the list of background jobs. |
UpdateBackgroundJobOptionsโ
| Prop | Type | Description |
|---|---|---|
parameters | string | JSON-encoded string containing the full update parameters object (id, notificationFrequency, notificationFrequencyGrouping, condition, value, notificationHeader, notificationBody, isActive) - a single serialized blob. |