Receive Health Connect webhooks in n8n

Connect an HC Sync webhook to an n8n Webhook node, preserve its raw body, and verify the signature in a JavaScript Code node before processing health data. An Android app must first upload permitted Health Connect records to HC Sync; n8n receives the server events.

Last updated: 2026-11-04. API source: HC Sync documentation.

How should you configure the Webhook node?

Use POST and enable Raw Body. Keep the binary property named data for the code below. Set Respond to Using Respond to Webhook Node so the request is acknowledged only after verification and durable acceptance. Register the active workflow’s public HTTPS production URL with HC Sync, replacing the example URL below.

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"

See the n8n Webhook settings and binary buffer helper. Confirm the raw binary property in a test execution; do not reconstruct bytes with JSON.stringify.

How do you verify in a Code node?

Use JavaScript, Run Once for All Items. This example targets a self-hosted deployment where the administrator allows the crypto built-in module and environment access for this trusted Code node. Set HCSYNC_WEBHOOK_SECRET privately and permit crypto with NODE_FUNCTION_ALLOW_BUILTIN=crypto in the execution environment. Never paste secrets into shared workflow JSON.

const { createHmac, timingSafeEqual } = require('crypto');
const item = $input.first();
const raw = await this.helpers.getBinaryDataBuffer(0, 'data');
const ts = item.json.headers['x-hcsync-timestamp'] || '';
const sig = item.json.headers['x-hcsync-signature'] || '';
const secret = $env.HCSYNC_WEBHOOK_SECRET;
if (!secret) throw new Error('Set HCSYNC_WEBHOOK_SECRET');
if (!/^\d+$/.test(ts) || Math.abs(Date.now()/1000 - Number(ts)) > 300
    || !/^[a-f0-9]{64}$/.test(sig)) throw new Error('Invalid signature');
const want = createHmac('sha256', secret).update(ts + '.').update(raw).digest();
if (!timingSafeEqual(Buffer.from(sig, 'hex'), want)) {
  throw new Error('Invalid signature');
}
const event = JSON.parse(raw.toString('utf8'));
if (!event || typeof event !== 'object' || Array.isArray(event)) {
  throw new Error('Invalid event');
}
return [{ json: event }];

When should the workflow respond?

After verification, insert the event into a durable inbox with a unique event ID, then use Respond to Webhook to return 204. Signed test events have no ID; route them to an acknowledgement without normal processing. Keep invalid-signature and storage-error paths from reaching the success response. Configure failures to return non-2xx and verify that behavior in your deployed workflow. Slow processing belongs after durable acceptance.

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.

Use a Postgres inbox for deduplication and apply deletion events to downstream health data. Consult the Node and Python verification guide to test matching signatures.

Frequently asked questions

Can the Webhook node respond immediately?

Wait until signature verification and durable acceptance before sending success; otherwise invalid or unprocessed events may be acknowledged.

Can I sign JSON.stringify of the parsed body?

No. The signature covers the timestamp, a dot and the original body bytes. Preserve the raw body.

Python guide · Expo guide · API documentation

Create a free project