---
title: Reading Step Counts on iOS and Android with Capacitor
description: Read the step count in a Capacitor app from HealthKit and Health Connect, with today's total from local midnight, 7-day buckets and no double counting.
date:
  created: 2026-10-08
  updated: 2026-10-08
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor Health: sdks/capacitor/health.md
faq: true
---

# Reading Step Counts on iOS and Android with Capacitor

A user's step count lives in the phone's health store, where the phone itself, a smartwatch and fitness apps each write their own step samples. The [Capacitor Health plugin](../../sdks/capacitor/health.md) reads that store through Apple HealthKit on iOS and Health Connect on Android, and one `aggregate(...)` call with the `day` bucket and the `sum` operation, starting at local midnight, returns the daily step count. This guide shows how to read the step count in a Capacitor app, from the availability check and the permission request to today's total, a 7-day chart and the reason you should never add up raw step records yourself. The plugin is part of [Capawesome Insiders](../../insiders/index.md).

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Thousands of teams ship faster with Capawesome Cloud Native Builds and Live Updates" src="https://capawesome.io/assets/banners/cloud-teams-ship-faster-with-capacitor.png" />
  </a>
</div>

**Key takeaways:**

- `aggregate(...)` with `DataType.Steps`, the `day` bucket and the `sum` operation returns daily step totals computed by HealthKit or Health Connect, with `null` for days without data.
- Buckets align to `startDate` and follow the device's time zone, so a range that starts at local midnight yields calendar days and "now minus 7 days" does not.
- Summing `readRecords(...)` counts phone and watch steps twice, while HealthKit statistics merge all sources and Health Connect keeps the highest-priority app's Activity data.
- On iOS, `checkPermissions(...)` never reports step read access as `granted`, so apps request access, query, and treat an empty result as "no data or no access".
- Health Connect allows reads up to 30 days before the first permission grant by default, and the Capacitor Health plugin 0.1.x has no background reads.

## Where steps come from

Step counts on iOS and Android come from the platform's health store: Apple HealthKit on iOS and Health Connect on Android. Neither store counts steps as a single number. It holds individual samples, each with a start date, an end date, a value and the app or device that wrote it. Apple's [`stepCount` documentation](https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier/stepcount){:target="_blank"} states that "the system automatically records samples on iPhone and Apple Watch", and a running app or a tracker's companion app can write its own samples for the same walk. A morning commute can therefore sit in the store two or three times.

On Android, Health Connect is part of the Android framework from Android 14 (API level 34). On Android 13 and lower, it is a separate app from the Google Play Store, which runs on Android 9 (API level 28) and later. That difference decides whether your app has to send the user to the Play Store first, which the next section handles.

The Capacitor Health plugin reads what these stores have recorded, including data recorded before your app was installed, within the limits listed under Platform limits. If you want to count steps yourself from the device's motion sensors while your app is open, the [fitness section of our device sensors guide](./capacitor-device-sensors-guide.md#fitness-health-tracking) covers that approach.

## Check availability

The health store is not available on every device, so the first call of a step feature is [`isAvailable()`](../../sdks/capacitor/health.md#isavailable). To install the Capacitor Health plugin, please refer to the [Installation](../../sdks/capacitor/health.md/#installation) section in the plugin documentation. The method resolves with `available` and a `reason`, which is `null` when the store is ready and one of three strings otherwise:

- `health-connect-not-installed`: the Health Connect app is missing (Android 9 to 13).
- `health-connect-update-required`: the installed Health Connect app is too old.
- `not-supported`: the device has no health store, for example Android below 9, iPadOS before 17 or visionOS.

For the first two reasons, [`installHealthConnect()`](../../sdks/capacitor/health.md#installhealthconnect) opens the Play Store page of Health Connect:

```typescript
import { Health } from '@capawesome-team/capacitor-health';

const ensureHealthStore = async () => {
  const { available, reason } = await Health.isAvailable();
  if (available) {
    return true;
  }
  if (reason === 'health-connect-not-installed' || reason === 'health-connect-update-required') {
    await Health.installHealthConnect();
  }
  return false;
};
```

The function returns `false` after opening the store, because the user installs the app outside your code. Call `isAvailable()` again when the user returns, and hide the step feature entirely for `not-supported`. On iOS, the check maps to HealthKit's `isHealthDataAvailable()`. On the web, every method of the plugin rejects with an unimplemented error.

## Request step permission

Reading steps requires a read permission for `DataType.Steps`, requested with [`requestPermissions(...)`](../../sdks/capacitor/health.md#requestpermissions). Each platform needs native configuration first:

- **Android**: declare `android.permission.health.READ_STEPS` in your own `AndroidManifest.xml`, since the plugin declares no health permissions and Google Play reviews every one you declare. Add the privacy policy `meta-data` entry from the plugin documentation and set `minSdkVersion` to 26.
- **iOS**: enable the HealthKit capability and add `NSHealthShareUsageDescription` to `Info.plist`. Apple's documentation warns that "your app will crash when you request authorization" without the usage keys, so the plugin checks for them first and rejects with an error instead.

With that in place, request only the step permission and read back its state:

```typescript
import { DataType, Health } from '@capawesome-team/capacitor-health';

const requestStepAccess = async () => {
  const { permissions } = await Health.requestPermissions({
    read: [DataType.Steps],
  });
  const steps = permissions.find((permission) => permission.dataType === DataType.Steps);
  return steps?.read;
};
```

On Android, `requestPermissions(...)` returns `denied` right after the user rejects the request, while [`checkPermissions(...)`](../../sdks/capacitor/health.md#checkpermissions) only ever reports `granted` or `prompt`. If the user denies the request twice, Health Connect ignores further requests, and only [`openSettings()`](../../sdks/capacitor/health.md#opensettings) lets the user grant access, so show a settings button once you see `denied`.

On iOS, the plugin never reports a read permission as `granted`: it reports `prompt` before the first request and `unknown` afterwards, because HealthKit hides read decisions to protect the user's privacy. [Apple's `authorizationStatus(for:)` documentation](https://developer.apple.com/documentation/healthkit/hkhealthstore/authorizationstatus%28for:%29){:target="_blank"} describes a denied read as one where "it simply appears as if there is no data of the requested type in the HealthKit store." The working pattern is to request access, run the step query and treat an empty result as "no data or no access" in your UI; the [Platform Behavior](../../sdks/capacitor/health.md#platform-behavior) table lists the reported states per platform.

## Today's total

Today's step count is one [`aggregate(...)`](../../sdks/capacitor/health.md#aggregate) call from local midnight until now. Build the start date with `setHours(0, 0, 0, 0)`, which sets the time to midnight in the device's time zone, and convert it with `toISOString()`:

```typescript
import { DataType, Health } from '@capawesome-team/capacitor-health';

const getTodaySteps = async () => {
  const startOfToday = new Date();
  startOfToday.setHours(0, 0, 0, 0);
  const { buckets } = await Health.aggregate({
    dataType: DataType.Steps,
    startDate: startOfToday.toISOString(),
    endDate: new Date().toISOString(),
    bucket: 'day',
    operations: ['sum'],
  });
  return buckets[0]?.values[0]?.value ?? null;
};
```

`toISOString()` always writes UTC, so in Berlin during summer time the start of October 8 becomes `2026-10-07T22:00:00.000Z`. That string still marks local midnight, because it is the same instant. `startDate` is inclusive and `endDate` is exclusive, so steps recorded at exactly midnight count toward today and the range ends at the moment of the call.

The result holds one bucket with one value per requested operation. The value is a plain number in the fixed unit of the data type, which is `count` for steps, and it is `null` when the bucket has no data. Keep `null` apart from `0` in your UI: on iOS, `null` can also mean that the user denied read access. `bucket: 'none'` skips the grouping and returns one bucket for the whole range, which gives the same number here.

## Daily and weekly buckets

A step chart for the last seven days is one `aggregate(...)` call with the `day` bucket and a start date at local midnight six days ago. The plugin aligns buckets to `startDate`, and the `day`, `week` and `month` buckets are calendar-aware and based on the device's time zone. The README sample starts at `Date.now()` minus seven days, so its buckets begin at the current time of day: called at 14:37, every bucket runs from 14:37 to 14:37 and mixes the steps of two calendar days. Starting at midnight fixes that:

```typescript
import { DataType, Health } from '@capawesome-team/capacitor-health';

const getWeekChartData = async () => {
  const start = new Date();
  start.setHours(0, 0, 0, 0);
  start.setDate(start.getDate() - 6);
  const { buckets } = await Health.aggregate({
    dataType: DataType.Steps,
    startDate: start.toISOString(),
    endDate: new Date().toISOString(),
    bucket: 'day',
    operations: ['sum'],
  });
  return buckets.map((bucket) => ({
    label: new Date(bucket.startDate).toLocaleDateString(undefined, { weekday: 'short' }),
    steps: bucket.values[0]?.value ?? 0,
  }));
};
```

`setDate()` moves the date in local time, so the start stays at midnight even when the week crosses a daylight saving change, which subtracting `6 * 24` hours in milliseconds would not. The buckets come back in chronological order, and the last one is today, which keeps growing. The chart maps `null` to `0` because a bar needs a number; keep the `null` if you want to draw missing days differently.

For longer views, switch the bucket to `week` or `month`. Since buckets align to `startDate`, start a weekly range at midnight on the day your week begins and a monthly range at midnight on the first of a month.

Steps support only the `sum` operation. Requesting `average`, `maximum` or `minimum` for `DataType.Steps` rejects with `INVALID_AGGREGATION` rather than resolving with empty results. A daily average is a calculation over the `day` buckets in your own code, where you decide whether days with `null` count.

## The double-counting trap

Adding up the step records from [`readRecords(...)`](../../sdks/capacitor/health.md#readrecords) gives a higher number than the user's health app shows whenever two sources recorded the same walk. A tempting implementation looks like this:

```typescript
import { DataType, Health } from '@capawesome-team/capacitor-health';

const sumStepRecords = async (startDate: string, endDate: string) => {
  const { records } = await Health.readRecords({
    dataType: DataType.Steps,
    startDate,
    endDate,
  });
  return records.reduce((total, record) => total + (record.value ?? 0), 0);
};
```

For a user who carries an iPhone and wears an Apple Watch, both devices write samples for the same minutes, and this function counts them twice. The plugin README describes this case: `aggregate(...)` "deduplicates overlapping data from multiple sources", and summing the records from `readRecords(...)` yourself counts the overlap twice.

The plugin does not do that merging itself. It passes the query to the platform. On iOS, HealthKit computes totals through statistics queries, and [Apple's `HKStatistics` documentation](https://developer.apple.com/documentation/healthkit/hkstatistics){:target="_blank"} describes the default:

> By default, these queries automatically merge the data from all of your data sources before performing the calculations.

On Android, Health Connect resolves duplicates through a priority list that the user sets. The [aggregate data guide](https://developer.android.com/health-and-fitness/guides/health-connect/develop/aggregate-data){:target="_blank"} (last updated September 23, 2026) states that "the Aggregate API accounts for any duplicate data and keeps only the data from the app with the highest priority." That rule covers the Activity and Sleep data types, and steps are an Activity type. Only the user can change that order, so your app cannot pick the winning source.

Raw records still have a job: showing where the steps came from. Each record carries `sourceBundleId`, the bundle identifier on iOS or the package name on Android, and `sourceName`, a readable name such as "Apple Watch" that is always `null` on Android. A per-source breakdown groups the records and labels each group:

```typescript
import { DataType, Health } from '@capawesome-team/capacitor-health';

const getStepsBySource = async (startDate: string, endDate: string) => {
  const { records } = await Health.readRecords({
    dataType: DataType.Steps,
    startDate,
    endDate,
  });
  const totals = new Map<string, number>();
  for (const record of records) {
    const source = record.sourceName ?? record.sourceBundleId ?? 'Unknown';
    totals.set(source, (totals.get(source) ?? 0) + (record.value ?? 0));
  }
  return totals;
};
```

Present these numbers as what each device or app recorded, never as parts of a total, because they will not add up to the `aggregate(...)` result. To read records from one app only, pass its identifiers in the `dataOrigins` option of `readRecords(...)`. `aggregate(...)` has no source filter, so a total always covers all sources.

## Platform limits

Three platform limits shape what a step feature can show:

- **History on Android**: Health Connect lets apps read data from up to 30 days before any permission was first granted. Google's [read data guide](https://developer.android.com/health-and-fitness/guides/health-connect/develop/read-data#read-older-data){:target="_blank"} (last updated September 24, 2026) names the `READ_HEALTH_DATA_HISTORY` permission for older data, and support for it is planned in the Capacitor Health plugin. The guide adds that reading records older than 30 days without it results in an error, so keep step ranges on Android inside that window.
- **No background reads**: the plugin reads steps while your app runs. It has no background read or observer API, so a widget or a daily summary notification has to refresh its numbers when the user opens the app.
- **On-device steps on Android 14+**: with Android 14 (API level 34) and SDK Extension version 20 or higher, Health Connect counts steps on the device itself once any app holds the `READ_STEPS` permission. `aggregate(...)` includes those steps automatically. Since a June 2026 update, Health Connect credits them to a device-specific package name instead of `android`, so a per-source list shows an unfamiliar package for them.

## FAQ

### How do I get a user's daily step count in a Capacitor app?

Call `aggregate(...)` of the [Capacitor Health plugin](../../sdks/capacitor/health.md) with `dataType: DataType.Steps`, `bucket: 'day'` and `operations: ['sum']`, using local midnight as `startDate`. HealthKit on iOS or Health Connect on Android computes the total and merges overlapping sources, and the bucket's value is `null` when no steps were recorded.

### Why is my sum of step records too high?

Because the phone, a watch and fitness apps write overlapping samples, and adding up the records from `readRecords(...)` counts the overlap more than once. Use `aggregate(...)` for any total and keep raw records for a per-source breakdown.

### Can I read steps older than 30 days on Android?

Not with the plugin today. Health Connect allows reads from up to 30 days before the first permission grant unless the app holds the `READ_HEALTH_DATA_HISTORY` permission, and support for that permission is planned in the Capacitor Health plugin.

### Do I still need Google Fit to read steps on Android?

No. Health Connect is the health store on Android, and Google supports the Fit APIs only until the end of 2026. [Migrating from Google Fit to Health Connect in Capacitor](./google-fit-to-health-connect-migration-in-capacitor.md) maps the Fit step data types to their Health Connect counterparts.

### Is the Capacitor Health plugin free?

No. The Capacitor Health plugin is part of the [Capawesome Insiders](../../insiders/index.md) subscription and requires Capacitor 8 or later.

## Related posts

- [Announcing the Capacitor Health Plugin](./announcing-the-capacitor-health-plugin.md)
- [Migrating from Google Fit to Health Connect in Capacitor](./google-fit-to-health-connect-migration-in-capacitor.md)
- [The Complete Guide to Capacitor Device Sensors](./capacitor-device-sensors-guide.md)

## Try Capawesome Cloud

Adding the Capacitor Health plugin changes native files on both platforms: the HealthKit capability and usage description on iOS, and the `READ_STEPS` permission in the Android manifest. Those changes ship with a new native build, and Capawesome Cloud builds and signs it for Android and iOS without a Mac. [How to Sign & Build Capacitor Apps in the Cloud](./how-to-sign-and-build-your-capacitor-app-in-the-cloud.md) shows the steps.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Use `aggregate(...)` for every step number your app displays, whether it's today's total, a weekly chart or a monthly goal, and start each range at local midnight. Reach for `readRecords(...)` only when the user wants to see which device or app recorded their steps. If you also write workouts or other health data, [Announcing the Capacitor Health Plugin](./announcing-the-capacitor-health-plugin.md) gives the overview of the other data types and the App Review requirements.

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
