Build a server for Syncky Health

Syncky Health is an iPhone app. It can send Apple Health data straight from the iPhone to an HTTP server you run: a home server, an automation tool or your own code. This guide describes everything that server sees and everything it may answer. It is written for a person and for an AI agent that builds the server.

For AI agents. Build the receiver from sections 3 to 8. Add section 9 if the server should print a setup link, and sections 10 and 11 if it should write data back into Apple Health. Section 14 has recipes for the app's templates, and section 15 is the final checklist. The vector files in test-vectors/ are the source of truth: webhook-requests.json (real requests in every format with their headers, secrets and setup link), webhook-exchanges.json (replies, the results request and the Check connection request) and webhook-signatures.json (signature checks). Every example below is copied from those files. When this text and a vector disagree, the vector wins. Test your server by replaying the vectors with their secrets and clock.

The quickest start is the example server in examples/minimal-server. It is one file with no dependencies, it implements every section of this guide, and its tests replay the same vectors.

1. What you are building

The app calls your server a server (in the data, a destination with an id like wh_…). Your server:

  1. accepts POST requests with a JSON, CSV or InfluxDB line protocol body;
  2. checks the token and, if signing is on, an HMAC signature;
  3. answers 2xx quickly;
  4. optionally answers with commands that the app writes into Apple Health, and then receives a results request that says what happened.

Nothing passes through Syncky's own servers on this route. One iPhone can have several servers. Each server belongs to one iPhone; there is no shared account.

What that means for responsibility: the server receives the user's health data in clear. HTTPS protects it in transit with TLS; plain HTTP does not encrypt it, on any network. Whoever runs the server is responsible for it under the data protection law that applies to them. Syncky's privacy policy (https://syncky.app/privacy) covers the app and the relay only. Syncky is a data-transfer tool, not a medical device; what a server does with the data is its operator's choice, and nothing in this guide is medical advice.

2. Set up the connection on the iPhone

In the app, open Settings › Connections › +. Then choose one of these:

Then tap Check connection. The app sends one test request with every header, auth and signature included, and no health data. There is no handshake or challenge step: this test is an ordinary request. Setting up and checking is free. Sending data needs a plan: Manual sends when you tap Sync now, Sync also sends automatically and is needed for writes to Health.

The server screen also has these settings:

3. The request

Every request is a POST to the URL you entered. Here are the headers of a native request with Bearer auth and signing on:

Content-Type: application/json
User-Agent: Syncky/1.0
X-Syncky-Format: native
X-Syncky-Delivery: <a new UUIDv7 for every request>
X-Syncky-Destination: wh_hmf4Hk2wv-D-TUbXkl-UCA
X-Syncky-Timestamp: <unix seconds>
X-Syncky-Signature: v1=<64 lowercase hex digits>
Authorization: Bearer <token>
Header Value
Content-Type application/json for native and Health Auto Export, text/csv; charset=utf-8 for CSV, text/plain; charset=utf-8 for InfluxDB
User-Agent Syncky/<app version>
X-Syncky-Format native, hae, csv or influx. Read the body by this header.
X-Syncky-Delivery a new UUIDv7 for every request. Use it as a request id.
X-Syncky-Destination the server's id in the app (wh_…). It names the server entry, not the iPhone.
X-Syncky-Timestamp, X-Syncky-Signature only when signing is on (section 6)
auth Authorization: Bearer <token>, Authorization: Basic <base64(user:password)> or your own header, for example X-Api-Key: <value>

The results request (section 10) has X-Syncky-Kind: results instead of X-Syncky-Format. Header names are not case-sensitive; many servers see them in lower case.

The Health Auto Export format adds the headers that Health Auto Export servers expect. automation-id is the server id, and session-id is one id for all requests of one delivery:

automation-name: Vector hae-every-shape
automation-id: wh_xG_XgGsxv7V7zu5FlJxMVQ
automation-aggregation: false
automation-period: Since Last Sync
session-id: <one UUIDv7 per delivery>

No header names the iPhone. In the native format the body's device field is the app's install id (d_…). In the other formats, give each iPhone its own server entry or its own URL path.

Rules for your URL:

Bodies are never compressed.

Deliveries and batching

One sync is one delivery to your server. A large delivery is split into several requests. Each body is at most 1,000,000 bytes. Requests are sent one after another, and the first failure stops the delivery. The data that did not get a 2xx is sent again next time.

Delivery is at least once. You will see repeats, so store samples by key:

4. Body formats

The body is one JSON object:

Times are RFC 3339 in UTC. A quantity value is in the type's unit from types.json. Percent types are fractions from 0 to 1 with the unit %. These samples come from one real request:

{"uuid": "5E1D0000-0000-4000-8000-000000000001", "type": "steps", "value": 412, "unit": "count",
 "start": "2026-10-14T07:01:00Z", "end": "2026-10-14T07:01:59Z",
 "source": {"app": "com.apple.Health", "device": "Apple Watch"}}

A category sample carries the value's name:

{"uuid": "51EE0000-0000-4000-8000-000000000001", "type": "sleep_analysis", "value": "asleep_core",
 "start": "2026-10-13T23:10:00Z", "end": "2026-10-14T00:40:00Z",
 "source": {"app": "com.apple.Health", "device": "Apple Watch"}}

A workout:

{"uuid": "0B6F0000-0000-4000-8000-000000000001", "type": "workout", "activity": "running",
 "start": "2026-10-14T05:00:00Z", "end": "2026-10-14T05:31:10Z", "duration_s": 1870,
 "energy_kcal": 312.4, "distance_m": 5020, "location": "unknown",
 "metadata": {}, "events": [], "activities": [], "statistics": [],
 "source": {"app": "com.apple.Health", "device": "Apple Watch"}}

A characteristic has a fixed uuid, so a new value replaces the old one:

{"uuid": "characteristic:blood_type", "type": "characteristic", "name": "blood_type", "value": "a_positive"}

The other kinds are state_of_mind, gad7_assessment, phq9_assessment, audiogram, activity_summary, workout_route and heartbeat_series. Routes and heartbeat series can be large, so they come in parts. Each part has part (from 0) and parts. A set is complete when every part is there and the joined points match count:

{"uuid": "0E0B0000-0000-4000-8000-000000000001", "type": "heartbeat_series",
 "start": "2026-10-14T06:00:00Z", "end": "2026-10-14T06:01:00Z", "count": 6, "part": 0, "parts": 1,
 "beats": [[0, 0], [842, 0], [1677, 0], [2519, 1], [3350, 0], [4188, 0]],
 "source": {"app": "com.apple.Health", "device": "Apple Watch"}}

The parts of one set can arrive in different requests. series-parts.json shows how to join them. The schema of the whole body is $defs/sync in schemas/inner-sync.schema.json. The native_* cases of webhook-requests.json hold complete bodies of every kind.

Health Auto Export JSON

This is the body of Health Auto Export's "REST API" automation, so a server written for that app can take it: {"data":{"metrics":[…],"workouts":[…]}}. The arrays stateOfMind and symptoms appear only when they are not empty. Metric names follow Health Auto Export, not the Syncky type names (steps becomes step_count). Dates look like 2026-10-14 09:01:03 +0200. Percent values are multiplied by 100. One metric:

{"name": "heart_rate", "units": "count/min",
 "data": [{"date": "2026-10-14 09:01:03 +0200", "Min": 64, "Avg": 64, "Max": 64, "source": "Apple Watch"}]}

One workout:

{"id": "0B6F0000-0000-4000-8000-000000000001", "name": "Running",
 "start": "2026-10-14 07:00:00 +0200", "end": "2026-10-14 07:31:10 +0200", "duration": 1870,
 "activeEnergyBurned": {"qty": 312.4, "units": "kcal"}, "distance": {"qty": 5.02, "units": "km"}}

This format has no deletions, no characteristics and no delivery object. Types without a Health Auto Export name are not sent. A workout can come again later with its route, so store workouts by id. The hae_* cases of webhook-requests.json show every shape and name. This format has not been checked against a live Health Auto Export receiver.

CSV

RFC 4180 with CRLF line ends. Every request starts with the same header row. A field with a comma, a quote or a line break is quoted. A time is RFC 3339 in the sample's own time zone when the iPhone knows it, else in UTC. These lines come from one real body:

type,uuid,start,end,value,unit,value_name,source,device,user_entered
steps,5E1D0000-0000-4000-8000-000000000001,2026-10-14T09:01:00+02:00,2026-10-14T09:01:59+02:00,412,count,,com.apple.Health,Apple Watch,false
sleep_analysis,51EE0000-0000-4000-8000-000000000001,2026-10-14T01:10:00+02:00,2026-10-14T02:40:00+02:00,3,,asleep_core,com.apple.Health,Apple Watch,false
workout,0B6F0000-0000-4000-8000-000000000001,2026-10-14T07:00:00+02:00,2026-10-14T07:31:10+02:00,1870,s,running,com.apple.Health,Apple Watch,false
characteristic,characteristic:blood_type,,,1,,a_positive,,,
steps,5E1D0000-0000-4000-8000-000000000099,,,,,deleted,,,

State of mind, assessments, audiograms, activity summaries, routes and heartbeat series are not sent in CSV.

InfluxDB line protocol

One line per sample, each ending in LF. The measurement is the type name. The tags are unit, source and device, sorted, and empty tags are left out. The timestamp is the sample's start in nanoseconds. These lines come from one real body:

steps,device=Apple\ Watch,source=com.apple.Health,unit=count value=412 1791961260000000000
sleep_analysis,device=Apple\ Watch,source=com.apple.Health value=3,value_name="asleep_core" 1791933000000000000
workout,activity=running,device=Apple\ Watch,source=com.apple.Health duration_s=1870,energy_kcal=312.4,distance_m=5020 1791954000000000000

Deletions, characteristics and the other kinds are not sent in line protocol.

The Check connection request

Check connection sends the format's body with no data. Your server should answer it like any request: check the auth and the signature, store nothing, answer 2xx. The app does not read the reply. This is the complete native request:

Content-Type: application/json
User-Agent: Syncky/1.0
X-Syncky-Format: native
X-Syncky-Delivery: 01a1393a-8960-7b26-8ec7-5d0493cc3337
X-Syncky-Destination: wh_hmf4Hk2wv-D-TUbXkl-UCA
X-Syncky-Timestamp: 1791961500
X-Syncky-Signature: v1=b5d4f350a4455a39d6e89cfb3e24d4b96fa4a47b7437b985fd8e8ad26c1baf3c
Authorization: Bearer vec_token_N_2Y13AA1iajwMKegGliR7uA-JPcykkrnx_oquTiDU0
{"v":1,"type":"sync","device":"d_zsOZnTQUz_juWFVljuOOBQ","samples":[],"deleted":[],"acks":[],"more":false,"delivery":{"destination":"wh_hmf4Hk2wv-D-TUbXkl-UCA","id":"01a1393a-8960-7b26-8ec7-5d0493cc3337","index":0,"count":1,"sent_at":"2026-10-14T07:05:00Z","test":true}}

Health Auto Export:

{"data":{"metrics":[],"workouts":[]}}

CSV, the header row alone (one line ending in CRLF):

type,uuid,start,end,value,unit,value_name,source,device,user_entered

InfluxDB line protocol, an empty body:

5. Data types and units

Type names are short snake-case names, for example steps, heart_rate, dietary_water and body_mass. They match ^[a-z][a-z0-9_]{1,63}$. The HealthKit identifier of each type is in types.json as hk.

The lists grow with new app versions. Do not hard-code them, and accept type names you do not know yet.

6. Authentication and signatures

The app supports four kinds of auth: none, Bearer, Basic and a custom header. Compare the token in constant time.

Turn on Sign requests (HMAC) for any server that can be reached from the internet. The app generates the secret: whsec_ followed by 43 base64url characters. You can also put your own secret in the setup link (section 9).

Check every signed request like this:

  1. Take the raw body bytes, before any JSON parsing.
  2. Compute HMAC-SHA256 with the key = the whole whsec_… string as UTF-8, over X-Syncky-Timestamp + . + the raw body.
  3. Write it as v1= and 64 lowercase hex digits.
  4. Compare it with the whole X-Syncky-Signature value in constant time.
  5. Accept only if the timestamp is within 300 seconds of your clock.

Uppercase hex, a v0= prefix, a missing v1= and a body changed by one byte must all fail. There is no nonce, and X-Syncky-Delivery is not covered by the signature. If you need replay protection inside the 300-second window, keep the X-Syncky-Signature values of the last 300 seconds and reject a repeat: every attempt, retries included, is signed again with a new timestamp.

A worked example: the Check connection request in section 4 was signed with the secret whsec_mgHtkTuuLB_wsJa221vi031K8GmN9QbtPNq_RV8uD8c. Your check must accept it with the clock at 1791961500 and reject it when one byte changes.

The same check in Node, with the reply signer of section 10:

import { createHmac, timingSafeEqual } from "node:crypto";

const hmacHex = (secret, prefix, body) => createHmac("sha256", Buffer.from(secret, "utf8")).update(prefix).update(body).digest("hex");

export function verifyRequest(headers, rawBody, secret, now = Math.floor(Date.now() / 1000)) {
  const timestamp = headers["x-syncky-timestamp"];
  const signature = headers["x-syncky-signature"];
  if (typeof timestamp !== "string" || !/^[0-9]{1,15}$/.test(timestamp) || typeof signature !== "string") return false;
  const expected = Buffer.from(`v1=${hmacHex(secret, `${timestamp}.`, rawBody)}`, "utf8");
  const given = Buffer.from(signature, "utf8");
  return given.length === expected.length && timingSafeEqual(given, expected) && Math.abs(now - Number(timestamp)) <= 300;
}

export function signReply(secret, delivery, t, replyBody) {
  return `t=${t},v1=${hmacHex(secret, `${delivery}.${t}.`, replyBody)}`;
}

rawBody is a Buffer with the exact bytes you received. In Express, for example, use express.raw({ type: "*/*" }) instead of express.json() on this route. webhook-signatures.json has a case for every rule above.

7. Deletions

When a sample is deleted in Apple Health, the app tells your server, unless Send deletions is off.

{"uuid": "5E1D0000-0000-4000-8000-000000000099", "type": "steps"}

A deletion can name a sample you never stored, for example one deleted before it was sent. Ignore it.

8. Responses, retries and limits

Your answer Code What the app does
2xx none The request counts as delivered. The body is read only for write-back (section 10).
3xx redirect Failure. Redirects are never followed.
401 or 403 auth_failed Failure.
413 payload_too_large Failure. The app halves the body limit for this server, down to 65,536 bytes, and the next attempt uses it.
429 rate_limited Failure.
any other 4xx http_4xx Failure.
5xx http_5xx Failure.
no answer in time timeout Failure.
no connection (DNS, TCP, TLS) network Failure.

After a failure the app waits before it tries again. Sync now ignores the wait. A success resets it. The app shows the server as Retrying with the last error. Answer 2xx as soon as the body is stored, and do slow work later.

Limit Value
Servers per iPhone 50
Server name 64 characters
URL 2048 characters
Token, password or header value 4096 bytes
Custom header name 256 characters
Basic user 256 characters, printable ASCII, no colon
Request body 1,000,000 bytes at first; each 413 halves it, down to 65,536 bytes
Request timeout 30 s
Reply the app reads 65,536 bytes; a longer reply is ignored
Signature window 300 s each way, for requests and for replies
Backoff after a failure 30 s, 2 min, 10 min, 1 h, then every hour

A custom header cannot be Host, Content-Type, Content-Length, User-Agent, Authorization, Transfer-Encoding, Upgrade, Connection, Keep-Alive, TE, Trailer or any name starting with X-Syncky-.

A setup link fills in the whole server entry. Your server prints it, and the user pastes it or scans it as a QR code in Settings › Connections › + › Paste a setup link.

The supported schemes are exactly syncky (Syncky), syncky-dev (Syncky Dev) and syncky-staging (Syncky β). Link builders default to syncky. In the app, paste and scan accept webhook setup links with all three schemes. For pair links to relay agents, the app refuses a link for another build. A tapped webhook setup link opens the build registered for its own scheme and shows the import review and confirmation; no server is added before you confirm.

syncky://webhook?v=1&name=<pct>&url=<pct>&format=native|hae|csv|influx&auth=none|bearer|header|basic
                [&header=<name>][&user=<pct>][&token=<pct>][&hmac=<whsec_…>][&types=<a,b>]

A link for a custom header and two types:

syncky://webhook?v=1&name=Vector%20header&url=https%3A%2F%2Freceiver.example%2Fhook%2Fheader&format=native&auth=header&header=X-Api-Key&token=vec_key_x9VHBdwKQo-KwlcOB9v4cwlAawCa0nF2NWQRO1qLDlI&types=heart_rate,steps

Every case of webhook-requests.json carries the link for its server, so you can test your encoder against them. Optional case-level url_scheme supplies the link builder's scheme; absent means syncky. It does not change request bodies or headers.

The link carries secrets and is not signed. Show it only to the iPhone's owner, best as a QR code on a local screen. The app shows the name, the host and the format for explicit confirmation before it imports. After the import it runs Check connection by itself.

To print a QR code in a terminal, pipe the link into a QR tool, for example with the example server:

./server.mjs link --host myserver.local | qrencode -t ansiutf8

10. Write-back: commands and results

Your server can add records to Apple Health. The app reads a reply only when:

Commands reach the iPhone only in the reply to a data request. When the iPhone has nothing new to send, it makes no data request, and your commands wait for the next one. An empty body means no commands. A reply the app cannot use is ignored; the delivery still counts as delivered, and the server's status keeps the reason (reply_ignored: and too_large, not_json, not_commands, unsigned or bad_signature).

Over plain HTTP the reply must be signed, and signing must be on for the server. The reply signature differs from the request signature: it has its own time and covers the request's delivery id.

X-Syncky-Signature: t=<unix seconds>,v1=<64 lowercase hex digits of HMAC-SHA256(secret, X-Syncky-Delivery + "." + t + "." + raw reply body)>

The time must be within 300 seconds of the iPhone's clock. Over HTTPS the reply signature is not checked.

An example. The app sent a native request with these headers:

X-Syncky-Delivery: 01a13940-fe30-7630-9d7f-9da6e6de1e3d
X-Syncky-Timestamp: 1791961923

The server is at http://receiver.local/hook/every-kind, with the secret whsec_mgHtkTuuLB_wsJa221vi031K8GmN9QbtPNq_RV8uD8c. It answers with two writes:

Content-Type: application/json
X-Syncky-Signature: t=1791961923,v1=73d9d986299402060d48ffe640a17e657067e1a9e853ccdd3136c5e74a5163f5
{"v":1,"type":"commands","commands":[{"command_id":"c_1","version":1,"op":"write","type":"dietary_water","value":0.25,"unit":"L","start":"2026-10-14T07:00:00Z","end":"2026-10-14T07:00:00Z"},{"command_id":"c_2","version":1,"op":"write","type":"apple_exercise_time","value":30,"unit":"min","start":"2026-10-14T07:00:00Z","end":"2026-10-14T07:00:00Z"}]}

The same two commands, one per line for reading:

{"command_id": "c_1", "version": 1, "op": "write", "type": "dietary_water", "value": 0.25, "unit": "L",
 "start": "2026-10-14T07:00:00Z", "end": "2026-10-14T07:00:00Z"}
{"command_id": "c_2", "version": 1, "op": "write", "type": "apple_exercise_time", "value": 30, "unit": "min",
 "start": "2026-10-14T07:00:00Z", "end": "2026-10-14T07:00:00Z"}

Rules for commands (schema: schemas/inner-commands.schema.json):

apple_exercise_time cannot be written, so the app rejects c_2 at once and writes c_1.

Results

After it handles your commands, the app posts a results request to the same URL. It has X-Syncky-Kind: results and no X-Syncky-Format:

Content-Type: application/json
User-Agent: Syncky/1.0
X-Syncky-Kind: results
X-Syncky-Delivery: 01a13941-7360-7ee0-91c5-e3ae2be0fb4e
X-Syncky-Destination: wh_hmf4Hk2wv-D-TUbXkl-UCA
X-Syncky-Timestamp: 1791961953
X-Syncky-Signature: v1=faf090fb6106803f7435919ab1383d8b52256a0d4b86e99d22b110a9d6e32132
Authorization: Bearer vec_token_N_2Y13AA1iajwMKegGliR7uA-JPcykkrnx_oquTiDU0
{"v":1,"type":"results","results":[{"command_id":"c_1","version":1,"status":"applied"},{"command_id":"c_2","version":1,"status":"rejected","reason":"type_not_allowed"}]}

Deleting a written record

To remove the water entry, answer a later data request with a delete. This reply answers a data request whose X-Syncky-Delivery was 01a13977-ecb0-7fbd-bd2a-03d2ebe40d71:

Content-Type: application/json
X-Syncky-Signature: t=1791965523,v1=42e6daf6d840b6ffbbb1b9e81c2f94c525743082b26f4eca1a7cec1034e68c40
{"v":1,"type":"commands","commands":[{"command_id":"c_1","version":2,"op":"delete","type":"dietary_water"}]}

Its result is c_1 with version 2.

Your own writes come back to you as new samples in later deliveries, like any other Health data. There is no filter for them. This has not been checked end to end.

11. Write-back walkthrough with the example server

This uses examples/minimal-server on a computer on the same Wi-Fi as the iPhone. It needs Node 22.15 or later.

  1. Create the token and the signing secret, start the server, and in a second terminal print the link as a QR code:

    ./server.mjs init
    ./server.mjs serve --bind 0.0.0.0
    ./server.mjs link | qrencode -t ansiutf8
    

    link uses this computer's name with .local. Pass --host <name>.local to change it.

  2. On the iPhone: Settings › Connections › + › Paste a setup link, scan the code and confirm. The app runs Check connection. The server logs a test request.

  3. Open the new server and turn on Accept writes to Health.

  4. Queue the two writes of section 10:

    ./server.mjs write dietary_water 0.25 L --at 2026-10-14T07:00:00Z
    ./server.mjs write apple_exercise_time 30 min --at 2026-10-14T07:00:00Z
    
  5. Tap Sync now. The first delivery sends the history window, and the server answers it with the reply of section 10, signed over that request's delivery id. If the history was already sent, add new data first (see step 7).

  6. The app rejects c_2 and writes c_1, then sends the results. ./server.mjs status now shows two results and no command waiting. The water entry is in the Health app.

  7. Run ./server.mjs delete c_1 and send new data, for example by logging a glass of water in the Health app. The next reply carries the delete of section 10, and the entry is gone.

The server keeps every command until a result for its latest version arrives, so a reply lost on the way is sent again.

12. When data arrives

The app sends:

Everything except Sync now needs the Sync plan. iOS decides when a locked iPhone wakes up, so expect bursts, not a fixed schedule. The first delivery to a new server sends the history window, the last 30 days by default.

13. Local network and remote access

A server at home can use plain HTTP on a local address. HTTPS is recommended for remote access; HTTP remains available for any valid host under the warning and confirmation rules in section 3.

14. Recipes

Each template in the app links to its recipe here. The details of the products below come from their documentation and are not verified by Syncky.

Home Assistant

Template: format native, URL http://homeassistant.local:8123/api/webhook/syncky-health, auth None.

Add this automation. It logs the number of samples and skips the Check connection request:

alias: Syncky Health
triggers:
  - trigger: webhook
    webhook_id: syncky-health
    allowed_methods: [POST]
    local_only: true
conditions:
  - condition: template
    value_template: "{{ not (trigger.json.delivery.test | default(false)) }}"
actions:
  - action: logbook.log
    data:
      name: Syncky Health
      message: "{{ trigger.json.samples | length }} samples, {{ trigger.json.deleted | length }} deletions"

Not verified by Syncky: the trigger options and the reply of Home Assistant.

InfluxDB

Template: format influx, URL http://influxdb.local:8086/api/v2/write?org=home&bucket=health&precision=ns, auth Bearer.

  1. In InfluxDB 2, create the bucket health in the organization home, or change org and bucket in the URL.
  2. Create an API token that can write to the bucket, and enter it as the token in the app.
  3. Keep precision=ns: the timestamps are in nanoseconds.

n8n

Template: format native, URL https://n8n.example.net/webhook/syncky-health, auth Header X-Api-Key.

  1. Add a Webhook node: HTTP method POST, path syncky-health.
  2. Set Authentication to Header Auth. Create a credential with the name X-Api-Key and a long random value. Enter the same value in the app.
  3. Use the production URL (/webhook/…). The test URL (/webhook-test/…) works only while the editor listens.
  4. Read the body from the node's output: body.samples, body.deleted and body.delivery.test.

For write-back, set the Webhook node to respond with a Respond to Webhook node. Respond with JSON: a commands message (section 10) with Content-Type: application/json. Over HTTPS the reply needs no signature. If the node answers with its default message instead, the app ignores it.

Not verified by Syncky: the n8n node names and options, and how n8n keeps the raw body for an HMAC check.

Node-RED

Template: format native, URL http://nodered.local:1880/syncky-health, auth None.

Build the flow http in (method POST, URL /syncky-health) → function → http response. Give the function two outputs:

const sync = msg.payload;
const samples = sync.delivery && sync.delivery.test ? [] : sync.samples;
return [{ ...msg, statusCode: 200, payload: "" }, samples.length > 0 ? { payload: samples, device: sync.device } : null];

The first output answers the iPhone with an empty body. The second carries the samples to the rest of your flow.

Not verified by Syncky: the Node-RED node options and the raw body.

Health Auto Export server

Template: format hae, URL http://hae.local:3001/api/data, auth Bearer.

Use this for a server built for Health Auto Export's "REST API" automation, for example HealthyApps' own server.

Not verified by Syncky: this format against a live Health Auto Export receiver, and the receiver's auth.

15. Receiver checklist