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.

RouteAuthWhat it does
POST /v1/projects {name, email}noneCreate a project; returns keys and a one-time recovery_code (stored hashed). Save it separately
POST /v1/sample-datasecret keySends 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 keyMint a device token that can write one user only
POST /v1/sync {records:[…]}user tokenUpsert 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 tokenRemove records deleted on the device
GET /v1/records?user_id&type&from&to&limitsecret or read keyRaw 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&tzsecret or read keyDaily 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 deletesList 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&tzsecret or read keyPlain-language check of one user's day: records by type and source, last upload, queued webhooks, and likely causes of gaps
GET /v1/projectsecret or read keyPlan, synced users this month, records, last sync, webhook queue. The dashboard shows the same
POST /v1/webhook/testsecret keySend a signed test event to your webhook now
PUT /v1/webhook {url}secret keyPublic 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/deliveriessecret or read keyLast 50 delivery attempts (event, id, delivered or failed, time taken) and the 24-hour success rate. Kept 7 days
POST /v1/webhook/replaysecret keyRe-send queued deliveries now, oldest first; returns {delivered, pending}
POST /v1/keys/recover {project_id, recovery_code}noneConsumes the saved code, rotates all keys and device tokens, and returns a replacement recovery code once. No email recovery.
POST /v1/keys/rotatesecret keyNew secret key, read key and webhook secret; every existing user token and read key stops working
DELETE /v1/projectsecret keyDeletes the project and all its records
GET /v1/billingsecret keyCurrent plan, subscription status, customer portal URL, renews_at, ends_at and signed checkout links for Starter, Indie and Studio
POST /mcpread 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.

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?

Follow Google's Health Connect publishing guidance for the current forms and requirements.

Which Health Connect constraints matter?

Service status: /status.

What limits apply?

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.