Docs
To integrate HC Sync, install the Android library, declare Health Connect permissions, mint a token per user on your server and read uploaded records through REST. The Android SDK is version 0.3.0. The guide also covers signed webhook verification, explicit consent, Google Play declarations and MCP client access.
Last updated: 2026-11-04. Source for the 2026 service details: HC Sync page details.
How do you install the Android library?
// settings.gradle.kts
dependencyResolutionManagement {
repositories { google(); mavenCentral(); maven("https://hcsync.dev/maven") }
}
// app/build.gradle.kts
implementation("dev.hcsync:hcsync:0.3.0")
Which Android permissions should you declare?
In AndroidManifest.xml, declare only the types you use, plus background reads if you want hourly sync. Play Console asks you to justify each health permission, and your privacy policy must say data goes to your servers.
<!-- AndroidManifest.xml: merge these entries into your app's manifest -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.READ_SLEEP" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<uses-permission android:name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND" />
<queries><package android:name="com.google.android.apps.healthdata" /></queries>
<application android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules">
<activity android:name=".PrivacyActivity" android:exported="true">
<intent-filter><action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" /></intent-filter>
</activity>
<activity-alias android:name="ViewPermissionUsageActivity" android:exported="true"
android:targetActivity=".PrivacyActivity" 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>
</application>
</manifest>
Using Expo? Skip this step: the hcsync-react-native config plugin writes these manifest entries for you.
Add PrivacyActivity.kt in your app package (replace com.example.app). Replace the sample policy with your actual collection, upload, retention and deletion policy before shipping.
package com.example.app
import android.app.Activity
import android.os.Bundle
import android.widget.TextView
class PrivacyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(TextView(this).apply {
text = "With your permission, this app reads steps, sleep and heart rate and uploads them through HC Sync to our server. Replace this text with your full privacy policy and contact details."
setPadding(32, 32, 32, 32)
})
}
}
Exclude hcsync.xml shared preferences from both cloud backup and device transfer: they hold the user token. Add these two files in app/src/main/res/xml/ (compileSdk 31 or newer for data extraction rules).
<!-- backup_rules.xml: Android 11 and earlier --> <full-backup-content> <exclude domain="sharedpref" path="hcsync.xml" /> </full-backup-content>
<!-- data_extraction_rules.xml: Android 12 and newer --> <data-extraction-rules> <cloud-backup><exclude domain="sharedpref" path="hcsync.xml" /></cloud-backup> <device-transfer><exclude domain="sharedpref" path="hcsync.xml" /></device-transfer> </data-extraction-rules>
How do you use the drop-in Connect screen?
One call shows availability (with install/update buttons), explains each data type, requests permissions (including optional background access) and runs the first sync with live progress. Light and dark, TalkBack-friendly, survives rotation.
val connect = registerForActivityResult(HcSync.connectContract()) { r ->
when (r) {
is ConnectResult.Connected -> toast("Synced ${r.uploaded} records")
is ConnectResult.Failed -> log(r.kind)
ConnectResult.Cancelled -> {}
}
}
connect.launch(ConnectOptions(appName = "My Coach", userToken = token,
types = setOf("steps", "sleep", "heart_rate"), privacyUrl = "https://myapp.com/privacy"))
Or build your own flow with the calls below.
How do you mint a token and start syncing?
# your server, with the secret key
curl -X POST https://hcsync.dev/v1/user-tokens \
-H "authorization: Bearer $HCSYNC_SECRET" -d '{"user_id":"maya"}'
// your app
HcSync.configure(context, userToken, types = setOf("steps", "sleep", "heart_rate"))
val ask = registerForActivityResult(HcSync.permissionContract()) { HcSync.start(context) }
ask.launch(HcSync.permissions(context))
HcSync.onRejected = { bad -> Log.w("hcsync", "rejected $bad") } // optional
HcSync.syncOnAppStart(application) // also sync when the app opens (at most every 15 min)
// Show sync health in your UI
lifecycleScope.launch { HcSync.state.collect { s ->
when (s) {
is SyncState.Synced -> status.text = "Synced ${s.uploaded} records"
is SyncState.Error -> when (val k = s.kind) {
is ErrorKind.Unavailable -> k.availability.intent?.let(::startActivity) // install or update Health Connect
is ErrorKind.PermissionDenied -> ask.launch(k.missing)
else -> status.text = s.message
}
else -> {}
}
} }
It backfills 30 days, then uploads only changes, hourly in the background. Without background access, call HcSync.syncNow(context) while your app is open; it returns the resulting SyncState. HcSync.logout(context) stops syncing and clears local tokens. A runnable sample app is in the SDK source (android/sample). Changes: changelog.
How do you read uploaded data?
Keys: the secret key does everything and stays on your server. The read key only reads; give it to dashboards and MCP clients. Device user tokens can only write one user.
| Route | Auth | What it does |
|---|---|---|
POST /v1/projects {name, email} | none | Create a project; returns keys and a one-time recovery_code (stored hashed). Save it separately |
POST /v1/sample-data | secret key | Sends 30 days of sample steps, sleep and heart rate for sample_user (deleted after 7 days) through the real pipeline (limits apply, your webhook fires). Try reads before wiring a phone; remove with DELETE /v1/users/sample_user |
POST /v1/user-tokens {user_id} | secret key | Mint a device token that can write one user only |
POST /v1/sync {records:[…]} | user token | Upsert up to 1,000 records (each value under 8 KB). Invalid records come back in rejected; the rest are stored. 402 when the plan's user limit is reached |
POST /v1/sync/delete {ids} | user token | Remove records deleted on the device |
GET /v1/records?user_id&type&from&to&limit | secret or read key | Raw records by start time, to exclusive (default last 7 days, limit 1–1,000). A full page returns next_cursor: repeat the query with cursor=next_cursor |
GET /v1/summary?user_id&days&tz | secret or read key | Daily steps, distance, active kcal, sleep/exercise min, sample-weighted bpm, resting bpm, weight. Days are local to tz (IANA, default UTC); sleep counts on the wake-up day |
GET /v1/users?cursor&user_id · DELETE /v1/users/{id} | read key lists · secret key deletes | List users (optional exact user_id search; 1,000 a page; pass next_cursor back as cursor) · delete all of a user's data |
GET /v1/explain?user_id&day&tz | secret or read key | Plain-language check of one user's day: records by type and source, last upload, queued webhooks, and likely causes of gaps |
GET /v1/project | secret or read key | Plan, synced users this month, records, last sync, webhook queue. The dashboard shows the same |
POST /v1/webhook/test | secret key | Send a signed test event to your webhook now |
PUT /v1/webhook {url} | secret key | Public https URL; x-hcsync-signature = hex HMAC-SHA256 of {x-hcsync-timestamp}.{body} with your webhook secret. Reject timestamps older than 5 min. Signed events with ids for dedupe. Queued events retry in order every 5 minutes, on sync or replay, for up to 7 days. The queue holds 500 events per project; new events are discarded when full. Record writes and event queuing are separate, so a failure between them can lose an event. REST exposes current records, but cannot recover missed deletion events. |
GET /v1/webhook/deliveries | secret or read key | Last 50 delivery attempts (event, id, delivered or failed, time taken) and the 24-hour success rate. Kept 7 days |
POST /v1/webhook/replay | secret key | Re-send queued deliveries now, oldest first; returns {delivered, pending} |
POST /v1/keys/recover {project_id, recovery_code} | none | Consumes the saved code, rotates all keys and device tokens, and returns a replacement recovery code once. No email recovery. |
POST /v1/keys/rotate | secret key | New secret key, read key and webhook secret; every existing user token and read key stops working |
DELETE /v1/project | secret key | Deletes the project and all its records |
GET /v1/billing | secret key | Current plan, subscription status, customer portal URL, renews_at, ends_at and signed checkout links for Starter, Indie and Studio |
POST /mcp | read key (give MCP clients this, not the secret key) | Tools: list_users, get_daily_summary, get_records |
How do you verify webhooks?
Install Express for the Node example or Flask for Python in your own receiver project. Set HCSYNC_WEBHOOK_SECRET. These minimal receivers acknowledge verified events; add a durable queue at the marked line before using them for real processing. Do not put JSON middleware before the Express raw route.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const secret = process.env.HCSYNC_WEBHOOK_SECRET;
if (!secret) throw new Error("Set HCSYNC_WEBHOOK_SECRET");
function verify(ts, sig, raw) {
if (!/^\d+$/.test(ts) || Math.abs(Date.now()/1000 - Number(ts)) > 300 || !/^[a-f0-9]{64}$/.test(sig)) return false;
const want = createHmac("sha256", secret).update(ts + ".").update(raw).digest();
return timingSafeEqual(Buffer.from(sig, "hex"), want);
}
// Register before express.json(): signatures cover the original bytes.
app.post("/hcsync", express.raw({ type: "application/json", limit: "1mb" }), (req, res) => {
if (!Buffer.isBuffer(req.body) || !verify(req.get("x-hcsync-timestamp") || "", req.get("x-hcsync-signature") || "", req.body)) return res.sendStatus(401);
let event;
try { event = JSON.parse(req.body.toString("utf8")); }
catch { return res.sendStatus(400); }
// Durably enqueue event here; deduplicate event.id (test events have no id).
// Return 2xx only after your queue accepts it. Handle deletions too.
return res.sendStatus(204);
});
app.listen(3000);import hashlib, hmac, os, re, time
from flask import Flask, request, abort
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1_000_000
SECRET = os.environ["HCSYNC_WEBHOOK_SECRET"].encode()
def verify(ts, sig, raw):
if not re.fullmatch(r"[0-9]+", ts) or not re.fullmatch(r"[a-f0-9]{64}", sig):
return False
try:
fresh = abs(time.time() - int(ts)) <= 300
except (ValueError, OverflowError):
return False
expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
return fresh and hmac.compare_digest(sig, expected)
@app.post("/hcsync")
def webhook():
raw = request.get_data(cache=True)
if not verify(request.headers.get("x-hcsync-timestamp", ""), request.headers.get("x-hcsync-signature", ""), raw):
abort(401)
event = request.get_json(force=True, silent=True)
if not isinstance(event, dict):
abort(400)
# Durably enqueue here; deduplicate event["id"] when present.
# Return 2xx only after your queue accepts it. Handle deletions too.
return "", 204
# Run with: flask --app receiver run --port 3000
Events: records.synced, records.deleted, user.deleted. Return any 2xx within 5 seconds. Queued deliveries are re-sent in order, signed afresh, for up to 7 days. The queue holds 500 events per project; new events are discarded when full. Record writes and event queuing are separate, so a failure between them can lose an event. REST exposes current records, but cannot recover missed deletion events. A test event is sent by POST /v1/webhook/test.
What explicit consent should your app collect?
Show a separate, unticked choice before uploading health data. Adapt the purpose and types to your app. Do not bundle health consent with accepting your Terms; record the choice and support withdrawal. Android permissions are a separate step.
☐ I explicitly consent to My Coach reading my steps and sleep from Health Connect and uploading them through HC Sync, stored in the US, to show my activity and sleep trends. I can withdraw at any time in Settings → Health connection. Withdrawal stops future uploads; I can request deletion of previously uploaded data there. See our Privacy Policy for retention, recipients and my rights.
Replace My Coach, the purpose, types and settings path with your actual behavior. Implement the withdrawal control: stop syncing, clear local credentials, and offer server deletion using your authenticated backend. Explain any lawful retention that remains. Do not upload if the user declines. Child-directed integrations for users under 16 require our written agreement.
What should you declare in Google Play?
- Complete the Health apps declaration in Play Console, explaining the feature and each requested Health Connect type. Update it when types or use change.
- Request only the permissions your feature uses; reconcile runtime requests, Expo types and the merged manifest.
- Publish an accessible privacy policy and permission rationale describing purposes, uploads through HC Sync, US storage, retention, deletion and withdrawal.
- Complete Data safety for your actual app: health data collected or shared, purposes, optional collection, encryption in transit and deletion options. Assess processors under Google's definitions; do not copy a generic answer.
- Test declined consent, revoked permissions, account switches, withdrawal and deletion in the release build. A plugin or SDK does not secure Play approval.
Follow Google's Health Connect publishing guidance for the current forms and requirements.
Which Health Connect constraints matter?
- Android 13 and lower need the Health Connect app from Google Play; on Android 14+ it is part of the system.
HcSync.availability()tells you which, with an install/update intent. - Background reads need the separate
READ_HEALTH_DATA_IN_BACKGROUNDpermission. Without it, sync runs only while your app is in the foreground: callsyncNow()on app start. - History: by default an app can read data from 30 days before the user granted permission. Older data needs
READ_HEALTH_DATA_HISTORY. - Change tokens expire after 30 days without a sync. The SDK then re-backfills 30 days; deletions made during the gap can't be detected.
- Source apps sync on their own schedule. Samsung Health, Fitbit and others write to Health Connect when they sync, so "today" can lag by hours. Check
sourceon each record. - Rate limits: Health Connect throttles heavy reads, especially in the background. The SDK reads changes, not full history, after the first backfill.
- Duplicates across sources: a phone and a watch can both record steps.
/v1/summaryranks sources by daily total for steps, distance and active calories (ties by source name). It splits at interval boundaries, uses the highest-ranked available source in each bucket, and fills gaps from others; overlapping records within that source add together. Values are prorated uniformly over each record interval. Non-overlapping intervals add together. Records use their start day; instantaneous values use the preferred source at that timestamp. Sleep and exercise durations use the union of intervals across sources; sleep excludes awake/out-of-bed stages before merging and uses the wake-up day; raw/v1/recordsreturns everything.
Service status: /status.
What limits apply?
- Free: 5 users, 200,000 records. Starter ($29): 100 users. Indie ($149): 1,000 users. Studio ($299): 20,000 users.
- Up to 1,000 records per sync call, each value under 8 KB. Records older than 13 months are deleted. See Security and data handling.
How do agent builders connect through MCP?
Connect a Streamable HTTP client to https://hcsync.dev/mcp with Authorization: Bearer $READ_KEY. Create a free project, save the read key, and load sample data in the dashboard. No email required.
Read-only tools: list_users, get_daily_summary(user_id, days), get_records(user_id, type, from, to). These share REST validation, project isolation and data limits. Records return at most 200 rows; daily summaries cover 1–90 days. Use REST pagination for larger exports. Legacy health_* names remain accepted aliases.
curl https://hcsync.dev/mcp -H 'Content-Type: application/json' \
-H 'Authorization: Bearer $READ_KEY' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_daily_summary","arguments":{"user_id":"sample_user","days":7}}}'Supports initialize, ping, tools/list and tools/call. JSON responses, no persistent sessions; notifications return 202. The existing IP limiter allows 120 authenticated MCP requests per hour and returns 429 with Retry-After. User device tokens cannot read. Keep health data and read keys private; configure your client’s authorization header instead of putting keys in prompts. MCP setup guide.
What else should you know?
Does HC Sync work on iPhone?
Not yet. Health Connect is Android. Apple HealthKit is next if you ask for it.
How do I delete a user’s data?
DELETE /v1/users/{id} removes everything for that user immediately.
Where is data stored?
Records are stored in the US and deleted after 13 months.