Destinations
Your own endpoint
POST any of five delivery formats.
2xxPoint Hozz at any endpoint that accepts a POST.
Mac, Home Assistant and InfluxDB use the same delivery mechanism with preset fields.
Setting it up#
Put the full URL in the address field#
Query string included, if you need one.
Add an authorization header if your endpoint wants one#
The secret field holds the entire header value. The header name defaults to
Authorization. It is stored in the device Keychain, not in the destination record.Choose a format#
All five are available. NDJSON is the default.
Send a test#
Sends a probe without Health data and reports the response.
What a request looks like#
POST /health HTTP/1.1
Host: example.com
Content-Type: application/x-ndjson
Idempotency-Key: 2f1a9c04-8e2b-4a1d-9f30-11c4e6a7b920
Hozz-Batch-Id: 2f1a9c04-8e2b-4a1d-9f30-11c4e6a7b920
Hozz-Batch-Sequence: 41
Hozz-Record-Count: 500
X-Hozz-Device: Brandon's iPhone
Authorization: <whatever you configured>
{"catalogVersion":6,"id":"…","kind":"quantity","…":"…"}
{"id":"9c40…","kind":"deletion","schemaVersion":1,"type":"HKQuantityTypeIdentifierStepCount"}| Header | Meaning |
|---|---|
| Content-Type | The format’s media type. |
| Idempotency-Key | The batch identifier. A repeat means the same bytes. |
| Hozz-Batch-Id | The same value, under a Hozz-specific name. |
| Hozz-Batch-Sequence | Batch number for this destination. |
| Hozz-Record-Count | How many records the payload holds. |
| X-Hozz-Device | What this phone calls itself. |
How to answer#
Return 2xx to accept. Hozz retries 408, 429 and 5xx; other responses stop for attention.
Idempotency#
The id derives from payload bytes. Identical retries reuse it; changed payloads get a new id.
A receiver, in full#
from http.server import BaseHTTPRequestHandler, HTTPServer
seen = set()
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
key = self.headers.get("Idempotency-Key")
body = self.rfile.read(int(self.headers["Content-Length"]))
# A repeated key means byte-identical content, so it is safe to drop.
if key not in seen:
seen.add(key)
for line in body.splitlines():
store(line) # your problem, not Hozz's
self.send_response(200) # any 2xx is an acceptance
self.end_headers()
HTTPServer(("", 8000), Handler).serve_forever()A dependency-free example lives in receiver/.
Choosing a format#
| Format | Media type | Notes |
|---|---|---|
| NDJSON | application/x-ndjson | One record per line. Lossless and streamable. |
| JSON | application/json | The same records as one array. |
| CSV | text/csv | Flat table. Drops metadata, devices, workout detail and route points. |
| Metrics JSON | application/json | Grouped by metric. Drops metadata, devices and workout detail. |
| InfluxDB line protocol | text/plain; charset=utf-8 | For InfluxDB and Telegraf. Drops metadata, workout detail and most device detail. |