Your iPhone pushes health data to an address the Console creates for it. Postern never pulls, and keeps what your phone sends from the day you start.
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 Sources → Add a source →
Apple Health.
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:
On the machine Postern runs on, run tailscale status. Read its Tailscale name. It
looks like gateway.example-tailnet.ts.net.
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
Delivery → Ingest 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_seconds0, time_in_bed_seconds0 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 TypeREST 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 Sources → Apple 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:
The address carries the connection id the Console printed.
The header holds the secret exactly.
The connection is not paused. Pause receiving stops new pushes. Data already
stored still answers.
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 TypeREST API and both on the same address. One exports health metrics, one exports
workouts.
{"error":"unauthorized"}, or nothing arrives at all
Check all four causes; they answer identically. The address carries the connection id the Console printed; the header holds the secret exactly; the connection is not paused (Resume receiving on its State row); the phone reaches port 8443.
The connection reads Last synced, but Samples cached stays at 0
The push arrived and stored nothing. Check the export version reads v2 in both automations, then run one by hand.
{"error":"payload too large"}
The push passed 25 MB, and none of it landed. Set the export file length back to one day (exportFileLength: "day") and turn batch requests off (batchRequests: false) in both automations. Postern records nothing for a push it refuses on size — not even in the record. The app’s own answer on the phone is the only place you will see this.
You did not copy the secret, or you shared an exported automation that held it
Open the connection → Delivery → Rotate secret. Every device on the old secret stops. Paste the new secret into both automations.
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.