Health Connect webhooks with signed delivery
HC Sync sends signed webhook events after Android uploads, record deletions and user deletions. Verify x-hcsync-signature as hex HMAC-SHA256 of x-hcsync-timestamp + dot + the original body bytes, reject stale timestamps, and durably accept events before returning 2xx.
Last updated: 2026-11-04. API source: HC Sync documentation.
How do you register and test a receiver?
curl -X PUT https://hcsync.dev/v1/webhook \
-H "authorization: Bearer $HCSYNC_SECRET" \
-H 'content-type: application/json' \
-d '{"url":"https://your-server.example/hcsync"}'
curl -X POST https://hcsync.dev/v1/webhook/test \
-H "authorization: Bearer $HCSYNC_SECRET"Use a public HTTPS URL and keep the webhook secret on your receiver. Test events are signed and have no event ID.
How do you verify in Node?
This is the Express example from docs. Set HCSYNC_WEBHOOK_SECRET and install Express. Add durable storage at the marked line before production use.
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);How do you verify in Python?
This Flask example also comes from docs. Set the same environment variable and install Flask. Both examples check timestamp format and freshness, signature format and a timing-safe digest comparison.
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 3000How do retries and replay work?
Return a 2xx within 5 seconds, after durable acceptance. Deduplicate event IDs; test events have no ID. Handle records.synced, records.deleted and user.deleted. Queued events retry in order every 5 minutes, on sync or replay, for up to 7 days, signed afresh. 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.
# Read recent attempts with a read key: curl https://hcsync.dev/v1/webhook/deliveries \ -H "authorization: Bearer $HCSYNC_READ_KEY" # Replay queued events, oldest first, with the secret key: curl -X POST https://hcsync.dev/v1/webhook/replay \ -H "authorization: Bearer $HCSYNC_SECRET"
Deliveries lists the last 50 attempts and the 24-hour success rate, retained for 7 days. Replay returns {delivered, pending}; it resends queued events, not an arbitrary historical event archive.
Frequently asked questions
Which events are delivered?
records.synced, records.deleted and user.deleted; POST /v1/webhook/test sends a signed test event.
Does replay recover every missed change?
No. Replay resends queued events. Queue limits and separate record writes mean events can be lost; REST cannot recover missed deletion events.