Skip to main content
Before you start
  • An iPhone, and the Health Auto Export app from the App Store. Its REST API automations may need the app’s paid tier. That is the app’s paywall, not Postern’s.
  • Remote access, set up first. Postern only answers requests from the machine it runs on. Your phone is a different machine, so open two ports to your own devices first. Tailscale’s private tier is enough. Nothing has to be public.
  • Postern is up, with the Console open at http://localhost:8787. Otherwise install Postern first. Allow about 10 minutes, plus your first export.
  • Your password manager open. The Console shows the device secret once.
1

Create the ingest endpoint in the Console

Open the Console at http://localhost:8787. Go to SourcesAdd a sourceApple Health.
The Console's Apple Health connect card, headed YOUR INGEST ENDPOINT. Its text: Postern mints a private ingest endpoint and a one-time secret for this device. Nothing is created until you ask. Below the text sits one button, Create your ingest endpoint.

The Apple Health card, before you press the button.

Postern cannot show you this secret again. It keeps a scrambled copy it can check against, never the secret itself. If you lose it, press Rotate secret on the connection. That makes a new secret and stops every device that still sends the old one. Open your password manager before you press the button.
Press Create your ingest endpoint. The card returns two values, then reads waiting for the first push:
  • Ingest path/api/ingest/<connection-id>
  • Device secret — shown once
Copy the secret into your password manager now.The ingest path is not a web address. Postern is never told your remote-access address, so you build the address yourself:
  1. On the machine Postern runs on, run tailscale status. Read its Tailscale name. It looks like gateway.example-tailnet.ts.net.
  2. Put https://, that name and :8443 in front of the path, exactly as the Console printed it. Port 8443 is the port you opened in Open two ports to your own devices.
Copy that address. It goes into your phone in step 3.Any address you put in front of the path works. The secret is what protects it. The ingest route is the one route Postern serves without a host check. The Console prints the same example with its own placeholder host, your-mac.tailnet.ts.net:8443. Both host names are fake. Use yours.
Leave this screen and the address stays. The connection page shows it again under DeliveryIngest endpoint, with a copy control. Only the secret is one-time.Come back here once a connection exists and the card reads A device is already pushing Health data here. Create another endpoint only if you’re adding a second device. Tick Yes — create a second ingest endpoint. and the button reads Create another endpoint. Take it literally: that is a second connection for a second device, not a repair. To fix the phone you already have, use Rotate secret instead.
2

Install Health Auto Export on the iPhone

On the iPhone, open the App Store and install Health Auto Export. Open the app.On first launch it asks for access to Apple Health. Grant every category you want exported. Without that access every export is empty, and every step after this one still looks like it worked.The app carries an Automations tab. The next three steps happen there.
Any program that can POST Postern’s JSON body with the secret in a header works. The Console’s Apple Health card carries that body under The JSON body Postern expects, with a Copy the template button. The same card describes an iPhone Shortcut built from a Get Contents of URL step. Four rules decide whether a payload lands:
  • Each metrics[].name becomes a metric type.
  • The metric named sleep_analysis becomes a sleep session.
  • Every entry under workouts[] needs its own id.
  • Dates are yyyy-MM-dd HH:mm:ss ±hhmm, not ISO. Postern skips a point it cannot date. It never stores one wrong.
The rest of this page uses the app. That is the path that runs in production.
3

Build the metrics automation

Open the Automations tab and press New Automation. Set Automation Type to REST API. The automation screen opens.Put the address you copied in step 1 into URL.Under Add Headers, add one header. Its name is Authorization. Its value is Bearer <your secret> — the word Bearer, one space, then the secret from step 1. A header named X-PCI-Webhook-Secret with the secret alone as its value works too, and it is the one the Console names.Under Data Type Settings, open Data Type and choose health metrics. An exported automation records that choice as "exportDataType": "healthMetrics".
Set the export version to v2 before you save. Postern reads the shape v2 sends: one data object that holds metrics and workouts. Postern refuses a body that carries no data object at all. Nobody has ever tested a push from an older export version, so leave it on v2.
Set the rest to match the phone that runs in production. The names below are the ones Health Auto Export writes into an exported automation, with the values that phone exports:
Automations, New Automation, Automation Type, REST API, URL, Add Headers, Data Type Settings, Data Type and Timeout Interval are the app’s own words, read off a capture of the phone in production on 1 August 2026.Nobody has recorded what the app calls the settings in the block above. Those are the key names an exported automation uses. Match them by what they do, and trust your screen over this page. requestTimeout is the app’s Timeout Interval field.
Select every metric. A day of metrics stays small. The app writes each name into the automation one by one, and the phone in production lists 113 of them. It stores a list, not a wildcard, so an app update that adds a new metric does not pick it up on its own — come back and tick it.Leave exportFileLength on day and batchRequests off. Postern refuses any single push over 25 MB with {"error":"payload too large"}, and nothing in a refused push lands. One day at a time has never come close on the phone in production.
Both aggregation settings change what an agent gets. Turn them on anyway — this is the pair the phone in production runs, and it has been healthy for weeks — but know the price.aggregateData: true changes the names. A point that carries a plain qty becomes one row under <name>. A point that carries Min, Avg or Max instead becomes one row per statistic present: <name>_min, <name>_avg, <name>_max. In production this happens to exactly one metric, heart rate: that store holds heart_rate_min, heart_rate_avg and heart_rate_max, and no heart_rate at all. An agent that asks for heart_rate gets nothing back. Postern tags every fanned row aggregation: daily_total, even when the value covers one minute. Never sum those rows across a day.aggregateSleep: true costs you the sleep numbers. Postern reads a night’s length from two fields that the aggregated shape leaves at 0, so a sleep session stores time_asleep_seconds 0, time_in_bed_seconds 0 and an empty efficiency_pct. The start and the end are right, and the real total survives under raw as totalSleep, in hours. An agent works the night’s length out from the start and the end.
The automation exists once it appears in the automations list. Nothing reaches Postern until it runs.
An exported automation carries your live address and secret in its JSON, in plain text, on disk wherever you export it. Share one and you hand someone a live key to your health data. You cannot unshare a secret. Remove the URL and the headers before a config leaves your phone, or rotate the secret afterwards.
4

Build the workouts automation

In the same Automations tab, press New Automation and set Automation Type to REST API again. Copy every setting from step 3 — the same address, the same header, and the whole block. Then change the four that differ:
On screen that is three moves: open Data Type and choose workouts, turn health metrics off in the include list, and turn workouts on. Then set Timeout Interval to 60.includeWorkoutMetadata and includeRoutes stay true in both automations. They do nothing in the metrics one, which carries no workouts.You now have two automations, both on Automation Type REST API and both on the same address: one exports health metrics, one exports workouts. Health Auto Export exports one data type per automation. With one automation, workouts never arrive.Sleep rides the metrics automation, not this one. Health Auto Export exports sleep as the metric sleep_analysis, and Postern turns that metric into sleep sessions.Postern stores workout routes and workout metadata in raw only. They are not columns an agent can query, and they make each push larger. Leave them on anyway: the phone in production has run includeRoutes and includeWorkoutMetadata on for weeks, one day per push, and no push has been refused on size.
5

Set the schedule, then run one automation by hand

Set each automation’s schedule. Hourly is a good default; the phone in production pushes every hour. Then run one automation by hand in the Automations tab, rather than wait for the schedule.In the Console, open SourcesApple Health. Until the first push lands the connection carries the badge Awaiting first push. When it lands, that badge disappears, the line reads Last synced with a time, and the caption beneath reads Device controlled. A healthy connection carries no badge, and no next time is predicted.Postern stamps Last synced on every push that arrives, even a push that stored no records.You can run the automation again safely. Health Auto Export’s export windows overlap on purpose. Postern gives every sample, sleep session and workout a fixed id. The same data sent twice updates those rows. It never duplicates them.Keep the gap between pushes under 48 hours. After 48 hours without a push the connection reads Stale. Stale is advice, not a fault. Your phone sends data to Postern when it decides to. Postern cannot ask for it, so there is no Sync now button, and Postern never calls a device Unreachable.
Nothing arrives, and nothing on the machine Postern runs on explains why. Every rejection at the ingest endpoint answers with the same 401 and the same body, {"error":"unauthorized"}. The answer never says which cause it was, so check all four:
  1. The address carries the connection id the Console printed.
  2. The header holds the secret exactly.
  3. The connection is not paused. Pause receiving stops new pushes. Data already stored still answers.
  4. The phone can reach the machine Postern runs on.
Every failure answers identically on purpose. A request that never arrives leaves no trace. From the phone, a network problem and a wrong secret look the same. The two curl checks in Open two ports to your own devices separate them.

Confirm it works

  • Console → Sources: one Apple Health connection, not two.
  • The connection page: Samples cached reads above zero.
  • The connection’s Delivery panel: Shared secret reads •••••••••••• set and a relative time — •••••••••••• set 3 days ago, or just now on a secret you made this minute. It is not a calendar date. It must not read no secret minted yet, and the button beside it must read Rotate secret, not Mint secret.
  • Health Auto Export → Automations: two automations, both on Automation Type REST API and both on the same address. One exports health metrics, one exports workouts.

If something went wrong

What you have now

Your phone sends health data straight to Postern, with no cloud in between. Postern refuses a push that does not carry the correct secret. An agent can read what your phone has sent since you set this up: sleep, workouts and every metric you selected. Two of those arrive renamed or thinned by the settings this page prescribes — what aggregateData and aggregateSleep cost. There is no history to backfill. Health is read-only here — a key granted health reads this data, and there is nothing to act on. Your phone decides when data moves. If the data looks old, run the automation on your phone. The secret lives in those two automations until you rotate it. Rotate it and every device on the old secret stops. Pause receiving stops new pushes; data already stored still answers. A paused connection refuses a push exactly as a wrong secret does.

Next

Connect Google and Gmail

your own Google Cloud app + one app password · about 20 minutes · weekly re-consent until you publish to production

Connect an agent

a key you created, granted health · a few minutes, plus a restart of the agent · one config block to paste