SyncFitSyncFitGet SyncFit
Developers

Your data, your endpoint.

SyncFit pushes exports to any HTTPS endpoint you control, writes to your database, and can serve data from the phone itself. This page is the complete contract.

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 request
POST /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 responseSyncFit behaviour
2xxSuccess. History records the status code. Incremental cursors advance.
429, 5xxRetried up to four attempts with exponential backoff (1s, 2s, 4s). Then reported as failed.
401, 403Failed immediately. Check the bearer token and headers.
413Failed. Lower the batch size in the destination or narrow the date range.
other 4xxFailed 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.

python
from 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()
shell
python3 receiver.py # find your Mac's address for the phone to use ipconfig getifaddr en0
  1. In SyncFit, add destination → REST API. Endpoint http://<your-mac-ip>:9090/export, method POST, bearer token test-token. Leave headers empty or add one to see it echoed.
  2. Create a job with a few metrics, CSV or JSON, and run it.
  3. 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:

output
192.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. None means 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" } ] }
FieldTypeMeaning
idUUID stringStable identifier from Apple Health. Use it as your primary key; re-deliveries carry the same id.
metricstringSyncFit metric identifier, e.g. heart_rate, step_count, sleep_analysis, workout. See identifiers.
start, endISO 8601Sample interval. Equal for instantaneous readings. Sleep and workouts span the session.
valuenumberNumeric value in unit. For category types (sleep stages, symptoms) this is Apple’s enum raw value. For workouts it is duration in seconds.
unitstringHealthKit unit string: count, count/min, kcal, km, kg, %, ms, degC and so on. Configurable per metric in the job.
minimum, maximum, averagenumber, optionalPresent only on aggregated samples.
source, sourceBundleIDstringRecording app or device and its bundle identifier. Empty when Include source information is off.
deviceobjectDevice name, model, hardware and software versions when known.
metadataobjectHealthKit metadata keys and values. Empty when Include metadata is off.
detailsobjectType-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.
aggregationstringraw, 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.

csv
uuid,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:

IdentifierUnitNotes
heart_ratecount/minDiscrete. Motion context in metadata.
resting_heart_ratecount/minOne per day.
heart_rate_variability_sdnnmsDiscrete.
oxygen_saturation%0–1 fraction in HealthKit; exported as percent.
step_countcountCumulative. Multiple sources overlap; combine one source per total.
distance_walking_runningkmCumulative.
active_energy, basal_energykcalCumulative.
apple_exercise_time, apple_stand_timeminCumulative.
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_masskgDiscrete.
body_fat_percentage%Discrete.
workoutsvalue is duration. details: activityType (HKWorkoutActivityType raw value), totalDistance, totalEnergy, route, heartRateData.
electrocardiogramcount/mindetails: classification, samplingFrequency, voltages.
state_of_mind–details: kind, valence, labels, associations. iOS 18+.
dietary_*g, kcal, mLNutrition, 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.

sql
CREATE 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.

behaviour
GET /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 compose
services: 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.