API reference

Base URL: https://location.evanrthomas.com (on the box: http://127.0.0.1:18810). Live, typed reference: /api-docs.

Auth

Token (env var in secrets/location.env) Used by How to send
LOCATION_API_TOKEN /location/*, /subscriptions*, /events Authorization: Bearer <t> only
LOCATION_OVERLAND_TOKEN POST /ingest/overland Bearer, or ?token=<t>
LOCATION_OWNTRACKS_TOKEN POST /ingest/owntracks Bearer, ?token=<t>, or HTTP Basic password (any username)

Wrong/missing token → 401. /healthz and /docs are public (no coordinates exposed).

cd ~/github.com/ethomas2/location-tracking && set -a && . secrets/location.env && set +a
B=https://location.evanrthomas.com
H="Authorization: Bearer $LOCATION_API_TOKEN"

Health

curl -s $B/healthz
# {"ok":true,"fixes":1234,"last_fix_at":"2026-10-02T21:03:11Z"}

Ingest

POST /ingest/overland

Overland's format: {"locations": [GeoJSON Feature, ...]}; each feature is a Point with coordinates: [lon, lat] and properties.timestamp (ISO 8601), horizontal_accuracy, battery_level, motion, device_id. Always answers {"result":"ok"} (even if some features were malformed and skipped). Resent batches are de-duplicated on (device, timestamp).

curl -s "$B/ingest/overland?token=$LOCATION_OVERLAND_TOKEN" -H 'Content-Type: application/json' -d '{
  "locations": [{
    "type": "Feature",
    "geometry": {"type": "Point", "coordinates": [-122.0839, 37.3861]},
    "properties": {"timestamp": "2026-10-02T14:00:00-07:00", "horizontal_accuracy": 12,
                   "battery_level": 0.8, "motion": ["walking"], "device_id": "iphone"}
  }]
}'
# {"result":"ok"}

POST /ingest/owntracks

OwnTracks HTTP mode (Settings → Connection → Mode HTTP, URL https://location.evanrthomas.com/ingest/owntracks, Authentication on, password = LOCATION_OWNTRACKS_TOKEN). Only _type: "location" messages are stored; answers []. Device name = last segment of topic, else the X-Limit-D header, else owntracks.

curl -s "$B/ingest/owntracks" -u "evan:$LOCATION_OWNTRACKS_TOKEN" -H 'Content-Type: application/json' \
  -d '{"_type":"location","lat":37.3861,"lon":-122.0839,"acc":10,"tst":1790000000,"batt":80}'

Location

A fix looks like:

{"id": 42, "lat": 37.3861, "lon": -122.0839, "accuracy_m": 12.0, "timestamp": "2026-10-02T21:00:00Z",
 "battery": 0.8, "motion": "walking", "source": "overland", "device": "iphone",
 "received_at": "2026-10-02T21:04:51Z"}
Endpoint Params Returns
GET /location/current device (optional) newest fix, or 404 if none
GET /location/history since, until (ISO 8601; no offset = UTC), limit (1–10000, default 100), device {"count": n, "fixes": [...]} newest first
DELETE /location/history device (required), since, until {"deleted": n}
curl -s -H "$H" $B/location/current | jq
curl -s -H "$H" -G $B/location/history --data-urlencode since=2026-10-02T00:00:00-07:00 -d limit=500 | jq '.count'

Subscriptions & events

Details and payloads: Subscriptions & callbacks.

Endpoint Notes
POST /subscriptions body {rule, target, callback_url?, secret?, note?, once?} → 201 subscription
GET /subscriptions active ones; ?include_inactive=true adds fired once subs
GET /subscriptions/{id} one subscription (secret is never returned; has_secret is)
DELETE /subscriptions/{id} 204; also drops its undelivered events
GET /events recent fired events + delivery status; limit, subscription_id
curl -s -H "$H" -H 'Content-Type: application/json' $B/subscriptions -d '{
  "rule": {"type": "geofence", "name": "Home", "lat": 37.3861, "lon": -122.0839, "radius_m": 150, "on": "both"},
  "callback_url": "https://example.com/hook", "secret": "change-me"
}' | jq
curl -s -H "$H" $B/subscriptions | jq
curl -s -H "$H" -X DELETE $B/subscriptions/<id> -o /dev/null -w '%{http_code}\n'
curl -s -H "$H" "$B/events?limit=10" | jq '.[] | {type, status, attempts, last_error}'