Guide · 10 min read
Apple HealthKit vs Google Health Connect for developers
Neither has a cloud API. How the two on-device stores differ in permissions, background delivery, history and what an aggregator can and cannot do for you.
Published · By the WearLink engineering team

If you searched for "Apple Health API" or "Health Connect API" hoping for a server endpoint, here is the fact that shapes everything else: neither Apple HealthKit nor Google Health Connect has a cloud API. Apple does not run a server that returns a user's heart rate. Google retired the one that did (Google Fit) and replaced it with an on-device store. Both are databases that live on the phone, readable only by an app the user installed and explicitly granted permission to.
Any vendor that implies server-side access to Apple Health, with or without an Apple logo on their homepage, is describing something impossible. That includes WearLink. What differs between vendors is what they do with the device side, and how well they handle the problems that follow from the data living on the phone: permissions that hide denial, intermittent connectivity, backfill cost, and duplicates from every app that writes to the store.
This guide compares the two stores at the level an experienced mobile engineer needs before committing to an architecture, then explains what an aggregator can and cannot honestly do for you.
Side by side
The two stores solve the same problem with different design choices. The table describes behaviour generically; exact API surfaces change with OS releases and the current platform documentation is the source of truth for specifics.
| Axis | Apple HealthKit | Google Health Connect |
|---|---|---|
| Platform | iOS and watchOS | Android |
| Where the data lives | On the device, in the HealthKit store. No Apple server you can call. | On the device, in the Health Connect store. No Google server you can call. |
| Server-side read | Impossible by design. Only an installed app with permission can read. | Impossible by design. The Google Fit REST API that once allowed it is deprecated. |
| Permission granularity | Per data type, read and write requested separately. | Per record type, read and write requested separately. |
| Is denial visible to the app? | Not for reads. A denied type returns nothing, the same as an empty type. | Yes, granted permissions can be queried. Empty data still returns nothing. |
| Background delivery | Observer queries with background delivery wake the app when new samples land. Frequency is throttled by the system and not guaranteed. | No equivalent push. The app reads periodically, typically from scheduled background work, and uses change tokens to fetch only what is new. |
| Historical depth | Whatever is in the store, often years, subject to the user’s own device and iCloud settings. | Limited. Treat the on-device window as short and back it up on your side. |
| Aggregates third-party apps | Yes. Oura, Strava, Garmin and others write into it, each record tagged with its source. | Yes. Garmin Connect, Samsung Health, Strava and others write into it, each record tagged with its data origin. |
| Permission UI | A system sheet listing every requested type, shown on first request. | A system permission screen owned by Health Connect, plus a rationale screen you must provide. |
| Availability | Built into the OS on every iPhone. | A separate component whose presence and version vary by Android version and device. Needs a graceful absent path. |
Two rows deserve emphasis. The background-delivery row decides your sync architecture: iOS can wake you when data changes, Android expects you to come back and ask. The historical depth row decides your storage architecture: if you need history beyond what the on-device window holds, your backend has to be the long-term store, and you have to pull data off the device before the window rolls.
Permissions: denied versus empty
Both stores ask the user per data type, so a user can grant steps and refuse heart rate. Where they differ is whether your app is told about the refusal.
HealthKit
HealthKit deliberately does not reveal read denials. The reasoning is privacy: if an app could see that a user refused to share a particular type, that refusal itself leaks something. The consequence for you is that a query against a denied type returns an empty result, indistinguishable from a type the user simply has no data for. You can find out whether you have asked for a type, which lets you avoid re-prompting, but you cannot find out the answer.
Health Connect
Health Connect has its own permission UI, separate from the ordinary Android runtime permission dialog, and it lets you query which permissions are currently granted. That removes the blind spot for denial. It adds two obligations: you must ship a rationale screen explaining why you want each type, and you must handle the case where Health Connect is not installed or is too old on the device.
On both platforms you need to explain the request before you make it. A permission sheet listing a dozen clinical-sounding types with no context produces denials, and the denials are hard or impossible to reverse from inside your app.
Getting data off the device
With a cloud provider, your server pulls whenever it wants. With an on-device store, the app pushes whenever it can. Everything below follows from that inversion.
Noticing new data
On iOS, register an observer query per type and enable background delivery. When new samples arrive the system may launch your app in the background, at which point you run an anchored query from the last anchor you stored and receive only the new and deleted samples. The wake-up frequency is throttled by the system, so treat it as best effort.
On Android there is no push. Schedule periodic background work, and on each run ask Health Connect for changes since the change token you stored last time. Tokens can expire if you wait too long between reads, in which case you fall back to a bounded time-range read.
Moving it to your backend
- Assume the network is absent. Queue records locally, upload in batches, and retry with backoff. A sync that happens on the next app open is normal, not a failure.
- Make upload idempotent. Key each record on the store's own record identifier plus the user. Retries and overlapping anchors will resend records; your backend must treat a resend as a no-op.
- Carry deletions. Both stores let users delete samples. An anchored query on iOS and a changes read on Android both report deletions, and your backend needs a way to receive them.
- Budget the first connect. The initial read is the expensive one: a user with years of Apple Watch history can have millions of samples. Backfill in chunks, oldest first or newest first depending on what your product shows, and cap how far back you go unless the user asks for more.
The record identifier is the piece people forget. The payload below is the shape the WearLink sync endpoint accepts; note that every item carries the on-device identifier and its source, because both are needed later.
{
"provider": "apple",
"sdkVersion": "1.0.0",
"syncTimestamp": "2026-09-01T06:00:00Z",
"data": {
"records": [
{
"id": "hk-record-uuid",
"type": "HKQuantityTypeIdentifierStepCount",
"startDate": "2026-09-01T05:00:00Z",
"endDate": "2026-09-01T06:00:00Z",
"value": 1240,
"unit": "count",
"source": { "deviceName": "Apple Watch", "deviceType": "watch" }
}
],
"workouts": [],
"sleep": []
}
}Duplicates are the normal case
Both stores are aggregators in their own right. Every app the user allows can write into them, and most wearable companion apps do. The result is that the same physiological event exists several times, and your pipeline will see all of them:
- An Apple Watch wearer who also wears an Oura ring has two sleep records for every night in Apple Health, one from watchOS and one written by the Oura app, with different stage boundaries.
- A Garmin user on Android has Garmin Connect writing workouts and daily steps into Health Connect, alongside the phone's own step counter.
- A run recorded on a watch, synced to Strava, and written back to the on-device store arrives at your backend from the store via your app and again from Strava via its cloud API, if you connect both.
Deleting duplicates is the wrong instinct, because you cannot know at write time which copy the user regards as canonical. The workable rule has two parts:
- Keep the origin on every record. HealthKit tags samples with the source app's bundle identifier and device; Health Connect tags records with a data origin. Store it. It is the only reliable way to tell "Oura's sleep" from "the Watch's sleep" later.
- Apply a provider-priority rule at read time. When two records of the same type overlap in time for the same user, prefer the origin with the higher priority and mark the other as a duplicate. Which provider wins is a product decision (the dedicated sleep device usually beats the watch for sleep, the watch usually beats the phone for steps), but it must be one rule applied everywhere, not a per-feature special case.
This is also why a step count from the on-device store should land in the same table, in the same shape, as a step count from Fitbit. If they are different objects, your priority rule has to be written twice.
What an aggregator can and cannot do
An aggregator cannot give you a cloud API for HealthKit or Health Connect. There is no server to proxy. If you have no mobile app, no vendor can get you Apple Health or Health Connect data, full stop.
What an aggregator can do is take the device side off your plate and make the output indistinguishable from its cloud providers: SDKs that read the on-device store, handle anchors and batching, and push into the same normalised pipeline as everything else, so a steps.created event from Health Connect and one from Fitbit are the same object with a different source, and the provider-priority rule above runs once.
WearLink's position on 1 September 2026, stated plainly:
| Store | Status | What exists today |
|---|---|---|
| Apple Health | On-device (SDK) | Swift SDK that reads HealthKit and pushes batches of records, sleep and workouts to the sync endpoint using a short-lived per-user SDK token minted by your backend. Ships as source; package-registry publishing is in progress. |
| Google Health Connect | On-device (SDK, preview) | Kotlin/JVM SDK published as source. The current preview surface is deliberately narrow (provider listing, user connections, activity summaries, meal-photo upload) and is expanding; Maven Central publishing is staged. |
| Samsung Health | On-device (SDK) | SDK push path into the shared schema. Samsung partner approval is required for the SDK itself; on recent Galaxy devices Health Connect is often the simpler route. |
| Apple Health XML export | Import (one-off) | Upload the ZIP the Health app exports, directly for small archives or via a presigned upload for large ones. Processing is asynchronous and emits the same normalised events (workout.created, sleep.created and so on) as records land. Useful for historical loads without a HealthKit permission round-trip. |
Decision guide
- No mobile app, or none planned: skip both stores. Use the cloud OAuth providers you can connect today (Oura, WHOOP, Fitbit, Strava, Withings), which your server reads without the phone being involved. Garmin, Polar, Suunto, Ultrahuman, Dexcom and Eight Sleep are in provider onboarding.
- iOS app, need Apple Watch data: HealthKit via the Swift SDK. Budget the permission UX and the first-connect backfill. Plan for silent denials.
- Android app, currently on Google Fit: you are on a deprecation clock. Follow the Google Fit migration guide, export history first, then move reads to Health Connect.
- Android app, many Galaxy users: start with Health Connect and check what Samsung Health already surfaces there before committing to the Samsung SDK and its partner approval.
- Both stores plus cloud providers: decide the provider-priority rule before you write the first record, and keep the data origin on everything. Retrofitting deduplication onto a year of mixed data is far harder than designing for it.
- Historical Apple Health data for a small cohort: the XML export import is often enough, and needs no app at all beyond a way for the user to hand you the file.
The short version: HealthKit and Health Connect are worth integrating precisely because they aggregate so much, and they are expensive to integrate for exactly the same reason. Neither will ever be a cloud API. Build for a device that is sometimes offline, a user who sometimes says no, and a record that usually exists twice.