How delivery works
Every export runs on the iPhone. SyncFit reads the selected Apple Health types, writes a file in the chosen format, then delivers that file to one destination. There is no SyncFit cloud in between: the request goes from your phone to your server.
- File destinations (REST, iCloud Drive, Files, email; Dropbox and Google Drive coming soon) receive the whole file in CSV, JSON or GPX.
- Database destinations (PostgreSQL, SQL Server) receive structured rows in batches inside a transaction, upserted by sample id.
Incremental jobs keep a cursor per metric and destination so only new samples are sent on the next run. Backfill jobs resend the full date range.
REST destination
Configure an endpoint URL (https for anything beyond your own network; plain http works for local testing), an HTTP method (POST by default, PUT supported), an optional bearer token stored in the iOS Keychain, custom headers, a timeout and a batch size. SyncFit sends the export file as the request body.
http requestPOST /export HTTP/1.1 Host: api.example.com Authorization: Bearer <your token> Content-Type: application/json # or text/csv when the job format is CSV User-Agent: SyncFit/1.0 X-Your-Header: value # any custom headers you configured {"schemaVersion":1,"samples":[ ... ]}
| Server response | SyncFit behaviour |
|---|---|
| 2xx | Success. History records the status code. Incremental cursors advance. |
| 429, 5xx | Retried up to four attempts with exponential backoff (1s, 2s, 4s). Then reported as failed. |
| 401, 403 | Failed immediately. Check the bearer token and headers. |
| 413 | Failed. Lower the batch size in the destination or narrow the date range. |
| other 4xx | Failed with the status code shown in History. |
Network timeouts and lost connections are retried the same way. Your server should be idempotent on sample id: a retry may deliver the same body twice.
Test it locally
You do not need a real backend to see what SyncFit sends. Run a tiny receiver on your Mac, point a REST destination at it from your iPhone on the same Wi‑Fi, and inspect the request. Save this as receiver.py and start it in a folder where you want the exports to land.
pythonfrom http.server import BaseHTTPRequestHandler, HTTPServer import time class Receiver(BaseHTTPRequestHandler): def do_POST(self): body = self.rfile.read(int(self.headers.get("Content-Length", 0))) ext = "json" if "json" in self.headers.get("Content-Type", "") else "csv" name = f"export-{time.strftime('%Y%m%d-%H%M%S')}.{ext}" with open(name, "wb") as f: f.write(body) print(self.command, self.path, "auth:", self.headers.get("Authorization"), len(body), "bytes ->", name) self.send_response(200) self.end_headers() self.wfile.write(b"ok") do_PUT = do_POST HTTPServer(("0.0.0.0", 9090), Receiver).serve_forever()
shellpython3 receiver.py # find your Mac's address for the phone to use ipconfig getifaddr en0
- In SyncFit, add destination → REST API. Endpoint
http://<your-mac-ip>:9090/export, method POST, bearer tokentest-token. Leave headers empty or add one to see it echoed. - Create a job with a few metrics, CSV or JSON, and run it.
- Watch the terminal. One line per request, and a new
export-…file next to the script.
Expected terminal output for a CSV job with a token set:
output192.168.1.23 - - [09/Sep/2026 13:59:20] "POST /export HTTP/1.1" 200 - POST /export auth: Bearer test-token 265014 bytes -> export-20260909-135920.csv
What to check:
- History in the app shows the job as succeeded with
HTTP 200. - auth shows
Bearer test-token.Nonemeans the destination was saved without a token. - The file opens as valid CSV (header row
uuid,metric,start,…) or JSON ({"schemaVersion":1,"samples":[…]}) and the row count matches the sample count in History. - Run the same job again in incremental mode: the second body is small or empty because only new samples are sent. A backfill job resends everything with the same ids.
To see the failure paths, change the response code in the script and run again. 500 or 429: the terminal shows four requests spaced 1, 2 and 4 seconds apart, then History reports the failure. 401: one request and an immediate failure naming the status code. Stop the script entirely: the job fails with a connection error after the same retry schedule.
This receiver stores nothing but files and has no authentication, so keep it on your own network. The reference receiver below is the next step when you want validation and a database.
JSON schema, version 1
The default JSON layout. One object with schemaVersion and a samples array. Timestamps are ISO 8601 with the job’s timezone offset.
json{ "schemaVersion": 1, "samples": [ { "id": "8F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8", "metric": "heart_rate", "start": "2026-09-08T07:41:12+10:00", "end": "2026-09-08T07:41:12+10:00", "value": 62, "unit": "count/min", "source": "Apple Watch", "sourceBundleID": "com.apple.health", "device": { "name": "Apple Watch", "model": "Watch", "hardwareVersion": "Watch7,4", "softwareVersion": "26.0" }, "metadata": { "HKMetadataKeyHeartRateMotionContext": 1 }, "details": {}, "aggregation": "raw" }, { "id": "0C9D8E7F-6A5B-4C4D-93E2-F1A0B9C8D7E6", "metric": "step_count", "start": "2026-09-08T00:00:00+10:00", "end": "2026-09-09T00:00:00+10:00", "value": 8421, "unit": "count", "minimum": 0, "maximum": 412, "average": 87.7, "source": "iPhone", "sourceBundleID": "com.apple.health", "device": {}, "metadata": {}, "details": {}, "aggregation": "day" } ] }
| Field | Type | Meaning |
|---|---|---|
| id | UUID string | Stable identifier from Apple Health. Use it as your primary key; re-deliveries carry the same id. |
| metric | string | SyncFit metric identifier, e.g. heart_rate, step_count, sleep_analysis, workout. See identifiers. |
| start, end | ISO 8601 | Sample interval. Equal for instantaneous readings. Sleep and workouts span the session. |
| value | number | Numeric value in unit. For category types (sleep stages, symptoms) this is Apple’s enum raw value. For workouts it is duration in seconds. |
| unit | string | HealthKit unit string: count, count/min, kcal, km, kg, %, ms, degC and so on. Configurable per metric in the job. |
| minimum, maximum, average | number, optional | Present only on aggregated samples. |
| source, sourceBundleID | string | Recording app or device and its bundle identifier. Empty when Include source information is off. |
| device | object | Device name, model, hardware and software versions when known. |
| metadata | object | HealthKit metadata keys and values. Empty when Include metadata is off. |
| details | object | Type-specific payload: workout activityType, route (array of lat/lon/altitude/time points), heartRateData; ECG voltages and classification; audiogram points; state of mind valence and labels. |
| aggregation | string | raw, hour, day, week or month, matching the job’s aggregation setting. |
The array is streamed, so a body can be large. Servers should stream-parse or enforce a size limit and ask users to lower the batch size.
CSV columns
Long layout, one sample per row, CRLF line endings, RFC 4180 quoting, UTF-8. Fields that could be interpreted as spreadsheet formulas are prefixed with an apostrophe.
csvuuid,metric,start,end,value,unit,min,max,avg,source,source_bundle_id,aggregation,metadata,details 8F1A2B3C-4D5E-4F60-8172-93A4B5C6D7E8,heart_rate,2026-09-08T07:41:12+10:00,2026-09-08T07:41:12+10:00,62.0,count/min,,,,Apple Watch,com.apple.health,raw,"{""HKMetadataKeyHeartRateMotionContext"":1}",{}
metadata and details are JSON objects encoded as strings. Column order is fixed; new columns, if ever added, are appended at the end.
Health Auto Export preview layout
For tools that already ingest Health Auto Export files, choose the Health Auto Export preview schema in a JSON job. Metrics are grouped under data.metrics with a name, units and data array, and workouts under data.workouts. Timestamps use yyyy-MM-dd HH:mm:ss Z. It is a compatibility preview and not a byte-for-byte reproduction of that product’s output.
json{ "data": { "metrics": [ { "name": "heart_rate", "units": "count/min", "data": [ { "date": "2026-09-08 07:41:12 +1000", "qty": 62 } ] } ], "workouts": [ ... ] } }
GPX
Available when a job exports only workouts. Each workout with a route becomes a <trk> with time-stamped track points; GPX 1.1, creator SyncFit. Workouts without a route are skipped.
Metric identifiers
Identifiers are lowercase snake_case derived from the HealthKit type name: HKQuantityTypeIdentifierHeartRateVariabilitySDNN becomes heart_rate_variability_sdnn. Commonly used identifiers:
| Identifier | Unit | Notes |
|---|---|---|
| heart_rate | count/min | Discrete. Motion context in metadata. |
| resting_heart_rate | count/min | One per day. |
| heart_rate_variability_sdnn | ms | Discrete. |
| oxygen_saturation | % | 0–1 fraction in HealthKit; exported as percent. |
| step_count | count | Cumulative. Multiple sources overlap; combine one source per total. |
| distance_walking_running | km | Cumulative. |
| active_energy, basal_energy | kcal | Cumulative. |
| apple_exercise_time, apple_stand_time | min | Cumulative. |
| sleep_analysis | – | Category. value 0 in bed, 1 asleep (unspecified), 2 awake, 3 core, 4 deep, 5 REM. Duration is end − start. |
| body_mass, lean_body_mass | kg | Discrete. |
| body_fat_percentage | % | Discrete. |
| workout | s | value is duration. details: activityType (HKWorkoutActivityType raw value), totalDistance, totalEnergy, route, heartRateData. |
| electrocardiogram | count/min | details: classification, samplingFrequency, voltages. |
| state_of_mind | – | details: kind, valence, labels, associations. iOS 18+. |
| dietary_* | g, kcal, mL | Nutrition, cumulative. |
The full list of over 190 identifiers is visible in the app’s metric picker; each one maps to a HealthKit type of the same name.
Database tables
SyncFit creates schema syncfit (configurable) with a metrics table and supporting tables for workouts, routes and notes. Rows are upserted by sample_uuid. Deleted Apple Health samples in incremental mode are removed. Schema migrations are applied on Test connection and before every export.
sqlCREATE SCHEMA IF NOT EXISTS syncfit; CREATE TABLE IF NOT EXISTS syncfit.metrics ( sample_uuid uuid PRIMARY KEY, metric text NOT NULL, start_at timestamptz NOT NULL, end_at timestamptz NOT NULL, value double precision NOT NULL, value_min double precision, value_max double precision, value_avg double precision, unit text NOT NULL, source_name text, source_bundle_id text, device_name text, device_model text, aggregation text NOT NULL, metadata jsonb, details jsonb, exported_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP );
SQL Server uses equivalent types (uniqueidentifier, datetimeoffset, nvarchar(max) for JSON). Full DDL for both dialects ships in the app repository under docs/postgresql.sql and docs/sqlserver.sql. Grant the SyncFit user CREATE on the schema for first setup; afterwards INSERT, UPDATE, DELETE and SELECT suffice.
Reference receiver
A minimal Node.js service that accepts SyncFit JSON and writes it to PostgreSQL or SQL Server. Use it as-is behind TLS, or as a starting point for your own endpoint.
behaviourGET /health → 200 {"status":"ok"} POST /export → requires Authorization: Bearer $SYNCFIT_TOKEN (24+ chars, constant-time compare) accepts {"schemaVersion":1,"samples":[...]} up to 16 MiB and 10 000 samples validates id (UUID), metric, value (finite), start/end (parseable), unit upserts every sample in one transaction → 200 {"accepted": 123} → 400 invalid JSON / schema / sample · 401 bad token · 413 too large · 500 database failure
docker composeservices: receiver: image: node:20-alpine working_dir: /app command: sh -c "npm ci && node src/index.js" volumes: [ "./server:/app" ] environment: SYNCFIT_TOKEN: change-me-to-a-long-random-secret DATABASE_URL: postgres://syncfit:secret@db:5432/health ports: [ "8080:8080" ] db: image: postgres:16 environment: { POSTGRES_USER: syncfit, POSTGRES_PASSWORD: secret, POSTGRES_DB: health }
Put it behind a TLS-terminating proxy (Caddy, nginx, Cloudflare Tunnel) and enter https://your-host/export plus the same token in the SyncFit REST destination. The receiver source is MIT licensed.
Questions about integrating? Read the troubleshooting FAQ first, then use the contact form with the “REST API or database integration” subject and include the HTTP status you received.