Format-Referenz

Für Developer und Self-Hoster · Version 1.0.2 · Stand: September 2026

Diese Seite richtet sich an Developer und Self-Hoster. Sie beschreibt, was die App an ein von dir eingetragenes Ziel schickt, damit du einen eigenen Empfänger bauen kannst. Alle Beispiele stammen wörtlich aus den Tests der App.

Suchst du stattdessen den Dateiexport (CSV, JSON, NDJSON zum Herunterladen), steht der weiter unten unter Dateiexport. Wie du ein Ziel überhaupt einträgst, steht in Ein Ziel einrichten.

Die HTTP-Schnittstelle

Ein Sync ist ein POST mit einem JSON-Body. Kein Chunking, keine Query-Parameter, kein Multipart, keine Kompression.

POST https://dein-server.at/health
Content-Type: application/json
X-Sender: export-health-data

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

Nur https://. Unverschlüsselte Adressen nimmt die App gar nicht erst an.

X-Sender ist immer export-health-data und ändert sich über Versionen hinweg nicht. Beliefern mehrere Programme dieselbe Adresse, erkennst du uns daran.

Dein Zugangsschlüssel — falls du einen einträgst — wird als frei benannter Header gesetzt, und zwar zuletzt. Nennst du dein Feld ebenfalls X-Sender, gewinnt deins.

Zugestellt heißt 2xx. Jeder andere Statuscode gilt als Fehlversuch. Antwortet deine Adresse mit einer Weiterleitung (301, 302, 303, 307, 308), gilt das als Fehler, nicht als Zustellung: Die App folgt der Weiterleitung nicht und schreibt den 3xx-Code ins Protokoll. Antworte mit 200, sobald du den Body gespeichert hast; auf den Inhalt deiner Antwort kommt es nicht an. Die ersten 200 Zeichen davon landen im Protokoll der App, was sich für eine kurze Quittung anbietet.

Timeout: 90 Sekunden. Danach bricht die App ab und wertet den Versuch als „keine Antwort". Speichere also erst den Body, dann antworte.

Wiederholungen: bis zu sechs Versuche, mit Grundabständen von 60, 300, 900, 3600 und 21 600 Sekunden, jeweils zufällig zwischen 50 und 100 % dieses Werts (Jitter). Ein Retry-After wird nicht ausgewertet.

Größe: im Betrieb 3,5 bis 5 kB je Sendung.

Der JSON-Envelope

Immer dieselben drei Schlüssel. workouts ist stets ein leeres Array — die App sendet keine Workouts, das Feld existiert nur der Kompatibilität wegen.

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

Auch ein leerer Lauf geht hinaus. Er ist kein Fehler, sondern ein Lebenszeichen:

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

Jedes Element in metrics hat genau drei Schlüssel in fester Reihenfolge:

{ "name": "…", "units": "…", "data": [ … ] }

Die Reihenfolge der Metriken ist bestimmbar: zuerst die Mengenmetriken alphabetisch nach name, danach heart_rate, zuletzt sleep_analysis. Innerhalb einer Metrik stehen die Punkte nach Tag aufsteigend.

Die zwölf Familien

Der automatische Versand trägt zwölf Familien — nicht die 188 Datentypen des Dateiexports. Achtung: drei Namen im Versand weichen von den naheliegenden ab. Verlass dich auf diese Spalte, nicht auf die Apple-Bezeichnung.

name                              units
step_count                        count
resting_heart_rate                bpm
heart_rate_variability            ms          ← nicht …_sdnn
vo2_max                           ml/kg/min
weight_body_mass                  kg          ← nicht body_mass
body_fat_percentage               %
lean_body_mass                    kg
respiratory_rate                  brpm
blood_oxygen_saturation           %           ← nicht oxygen_saturation
apple_sleeping_wrist_temperature  celsius
heart_rate                        bpm
sleep_analysis                    hr

Eine Familie erscheint nur, wenn es für sie Werte gibt. Rechne nicht damit, dass alle zwölf in jeder Sendung stehen.

Die drei Arten von Datenpunkten

Ein Datenpunkt sieht je nach Familie unterschiedlich aus. Wer nur die erste Art implementiert, verliert Puls und Schlaf.

1 · Mengenmetrik

Alle zehn Mengenmetriken, ein Punkt je Tag.

{"date":"2026-08-02 12:00:00 +0200","qty":97.4,"source":"healthpipe","sources":["com.apple.health.<ID>"]}

sources (ab Version 1.0.1) nennt die Bundle-IDs aller Quellen, die in den Tageswert eingeflossen sind — sortiert, ohne Doppelte. Der Schlüssel ist immer da: [] heißt „die App hat den Tag gelesen, aber keine Quelle hatte dafür Werte“ — eine leere Familie ist kein Fehler. source bleibt healthpipe. Apple-Quellen tragen je Gerät eine eigene, zufällige Kennung — welche davon die Uhr ist, steht nicht darin. Nur die ältere Version 1.0.0 sendet sources noch nicht — seit dem Update 1.0.1 (7. September 2026) ist der Schlüssel dabei.

2 · heart_rate

Kein qty, dafür drei großgeschriebene Felder — Tages-Minimum, -Mittel und -Maximum.

{"date":"2026-08-02 12:00:00 +0200","Min":44,"Avg":61.5,"Max":142,"source":"healthpipe","sources":["com.apple.health.<ID>","com.whoop.iphone"]}

sources wie bei der Mengenmetrik — hier die Quellen, aus denen Minimum, Mittel und Maximum des Tages gebildet wurden.

3 · sleep_analysis

Eine Nacht je lokalem Tag. date ist hier nicht der Mittagsstempel, sondern identisch mit sleepEnd — die Nacht gehört dem Tag, an dem sie endete.

{"date":"2026-08-02 06:40:00 +0200",
 "sleepStart":"2026-08-01 23:10:00 +0200",
 "sleepEnd":"2026-08-02 06:40:00 +0200",
 "asleep":7.1667,"totalSleep":7.1667,
 "deep":0.8333,"rem":2,"core":4.3333,"awake":0.3333,
 "source":"Apple Watch von Harald",
 "candidates":["com.apple.health.<ID>"]}

Fehlende Felder sind nicht null. Hat die gewählte Quelle keine echten Schlafstadien — etwa die Bewegungserkennung des iPhones —, fehlen deep, rem und core vollständig. awake erscheint nur, wenn es größer als null ist. Wer fehlende Felder als 0 einträgt, erfindet Daten.

asleep und totalSleep tragen immer denselben Wert. Nimm eines von beiden, nicht zwei Zeitreihen.

candidates (ab Version 1.0.1) nennt die Bundle-IDs aller Quellen, die für diese Nacht eine Kandidaten-Sitzung hatten — sortiert, ohne Doppelte, auch die nicht gewählten Quellen. source bleibt der Name der gewählten Quelle. Wurde zum Beispiel für eine Nacht Oura gewählt, obwohl auch WHOOP sie aufgezeichnet hat, steht dort:

"source":"Oura","candidates":["com.ouraring.oura","com.whoop.iphone"]

Nur die ältere Version 1.0.0 sendet candidates noch nicht — seit dem Update 1.0.1 (7. September 2026) ist der Schlüssel dabei.

Weitere Schlüssel

Punkte sind offene Objekte und können weitere Schlüssel tragen. sources und candidates sind mit 1.0.1 dazugekommen, jeweils am Ende des Punkts nach source — alles davor ist unverändert. Lies Punkte über benannte Schlüssel und überspring, was du nicht kennst. Ein Empfänger, der an einem unbekannten Schlüssel scheitert, steht außerhalb der Spezifikation.

Zeit, Zahlen, Einheiten

Das Zeitformat ist kein ISO-8601. Der Offset steht ohne Doppelpunkt:

yyyy-MM-dd HH:mm:ss ±HHMM      2026-08-02 12:00:00 +0200

Strenge ISO-Parser scheitern daran. In PHP passt 'Y-m-d H:i:s O', in Python '%Y-%m-%d %H:%M:%S %z'.

Tagespunkte tragen 12:00 Uhr Ortszeit, nicht Mitternacht. Das ist Absicht: Wer den Kalendertag aus dem Zeitstempel ableitet, kann ihn an einer Zeitumstellung nicht verlieren — mittags sind es zu beiden Tagesgrenzen zwölf Stunden. Der Offset selbst wird ebenfalls zur Mittagszeit des jeweiligen Tages bestimmt, Tage beiderseits einer Umstellung tragen also ihren eigenen.

Zahlen haben höchstens vier Nachkommastellen, nachlaufende Nullen werden abgeschnitten. Deshalb steht im Beispiel oben "rem":2 und nicht 2.0000. Behandle jedes Zahlenfeld als Fließkommazahl.

Prozentwerte kommen bereits als 0–100 mit der Einheit % — 97.4 bedeutet 97,4 %. Multipliziere nicht noch einmal.

Schlafwerte sind in Stunden angegeben (Einheit hr), nicht in Sekunden oder Minuten.

Das Feld source trägt bei Mengenmetriken und heart_rate konstant healthpipe — ein eingefrorener Bezeichner aus dem ursprünglichen Format, kein Gerätename. Nur bei Schlafpunkten steht dort der echte Name des Geräts, das die Nacht geschrieben hat, und der kann von Nacht zu Nacht wechseln. Rechne damit, dass dieses Feld dort personenbezogen ist. Die Listen sources und candidates tragen dagegen ausschließlich Bundle-IDs (com.ouraring.oura, com.whoop.iphone, com.apple.health.…), nie Gerätenamen.

Was dein Empfänger können muss

Zustellung ist at-least-once, nicht exactly-once. Zwei Gründe: Die Lesefenster überlappen sich — die App schickt die letzten drei Tage mit, damit Lücken sich von selbst füllen —, und ein Versuch, dessen Antwort verlorengeht, wird wiederholt. Derselbe Tag kommt also mehrfach, mit gleichem oder korrigiertem Wert.

Beim Lesefenster gibt es eine Ausnahme. heart_rate und step_count tragen die meisten Rohwerte und werden deshalb bei jedem Lauf, den nicht du selbst auslöst — auch bei einer Automation —, nur einen Tag zurück gelesen. Die vollen drei Tage bekommen diese beiden nur, wenn jemand in der App auf „Jetzt senden“ tippt. Eine Lücke bei Puls oder Schritten, die älter als ein Tag ist, schließt sich also nicht von selbst; alle übrigen Familien holen sich drei Tage.

Speichere deshalb nicht anhängend, sondern aktualisierend: ein Schlüssel aus Metrikname, Tag und gegebenenfalls Statistik-Art, und beim zweiten Mal überschreiben. Wer stumpf einfügt, hat nach einer Woche jeden Tag drei- bis viermal.

Unveränderte Sendungen bleiben aus. Ist der Inhalt zeichengleich mit dem zuletzt bestätigten, spart die App sich den Versand — außer du tippst „Jetzt senden", dann geht er immer hinaus. Bleibt es still, ist das also kein Fehler.

Antworte schnell und schreib vorher weg. Der Timeout liegt bei 90 Sekunden; danach kommt dieselbe Sendung später noch einmal.

Dateiexport

Der Weg über den Tab „Export" erzeugt eine Datei statt einer Sendung — andere Regeln, anderes Format.

CSV (Tab „Export", Knopf „Tabelle erstellen"): vier Spalten, Tag;Metrik;Wert;Einheit, eine Zeile je Tag und Metrik. UTF-8 mit BOM, Zeilenenden CRLF. Trennzeichen und Dezimalzeichen hängen am Regionalformat — EU heißt Semikolon und Komma, US heißt Komma und Punkt. Details und die Excel-Fallen stehen in Tabelle in Excel & Numbers öffnen.

JSON und NDJSON (nur über „Erweiterter Export"): Rohmessungen statt Tageswerte, alle 188 Datentypen wählbar. Sie ignorieren das Regionalformat vollständig — immer Punkt als Dezimalzeichen, immer ISO-Zeitstempel mit Doppelpunkt im Offset. Achtung: Prozentwerte stehen dort als Bruch (0.974, Einheit pct), nicht als 97,4 wie im Versand. NDJSON ist ein Objekt je Zeile ohne BOM, JSON ein Dokument, das so beginnt:

{"schema":"healthpipe.samples.v1", …}

Was diese Seite nicht verspricht

Das Versandformat ist eingefroren, weil bestehende Empfänger es lesen — es wird sich nicht ohne Not ändern. Eine formale Zusicherung ist das trotzdem nicht: Kommt eine Familie dazu, steht sie irgendwann in dieser Tabelle; kommt ein Schlüssel dazu, steht er bei der betreffenden Art von Datenpunkt. Bau deinen Empfänger so, dass ein unbekannter name — oder ein unbekannter Schlüssel in einem Punkt — nicht zum Abbruch führt, sondern übersprungen wird.

Und: Ob dein Ziel die Daten nach dem 2xx wirklich gespeichert hat, sieht die App nicht. „Zugestellt" heißt ausschließlich, dass dein Server den Empfang bestätigt hat.

← Zur Startseite