Groundpost runs a local REST API so your own tools — position estimators, spectrum scanners, fleet scripts — can draw markers, range rings, lines, tracks, pulse animations, directional arrows, and antenna coverage sectors directly on the operator's map. Push JSON, watch it render live. No plugin system, no SDK: anything that can speak HTTP can publish a layer.
Base URL http://127.0.0.1:8580/api/v1 · enabled by default · configure under Sharing
01 / Quickstart
The API is on by default at 127.0.0.1:8580. If you turned it off, open the Sharing tab, find HTTP API, and press Start API — the port is editable there while stopped.
curl -s http://127.0.0.1:8580/api/v1/ping
# {"ok":true,"v":1}
curl -s -X PUT http://127.0.0.1:8580/api/v1/layers/demo -d '{
"name": "demo",
"default_ttl_s": 60,
"items": [
{"kind": "marker", "id": "m1", "lat": 50.4497, "lon": 30.5245,
"shape": "diamond", "color": "#ff8c00", "label": "TARGET"},
{"kind": "circle", "id": "c1", "lat": 50.4497, "lon": 30.5245,
"radius_m": 500, "color": "#ff8c00", "label": "500 m"}
]}'
# {"ok":true}
Switch to the Map tab: an orange diamond labeled TARGET with a 500-meter ring around it. The ring is ground meters — it scales as you zoom. A Layers (1) button appears in the map toolbar to toggle visibility.
# PUT again with the same layer id — the layer is replaced atomically.
# Publishers typically re-PUT their full state at 1-5 Hz.
curl -s http://127.0.0.1:8580/api/v1/layers
# {"layers":[{"id":"demo","name":"demo","items":2,"last_update_s":4.2}]}
curl -s -X DELETE http://127.0.0.1:8580/api/v1/layers/demo
# {"ok":true}
Or just stop sending: every item carries a TTL (60 s here), so a dead publisher's data expires off the map instead of lingering as stale truth.
02 / Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/ping | Liveness + API version |
| GET | /api/v1/layers | List layers: id, name, live item count, seconds since last update |
| PUT | /api/v1/layers/{id} | Create or fully replace a layer (idempotent) |
| GET | /api/v1/layers/{id} | Current live (non-expired) layer content — handy for debugging |
| DELETE | /api/v1/layers/{id} | Remove a layer immediately |
Lowercase [a-z0-9_-], 1–32 chars. One id per publisher is the usual pattern (beacon, scanner, …).
16 layers · 512 items/layer · 4096 points/polyline · 1 MiB body. Over a limit → 413. Schema problem → 400 with the offending item index: {"error":"color must be #rrggbb or #rrggbbaa","item":3}. Nothing is silently truncated.
03 / Item Types
Every item has a stable id, a color (#rrggbb or #rrggbbaa), and an optional ttl_s overriding the layer's default_ttl_s. Labels render on a dark pill so they stay readable over snow and sand imagery.
{"kind": "marker", "id": "twr-210", "lat": 50.385, "lon": 30.061,
"color": "#ff6384", "shape": "circle", "label": "210",
"label_pos": "above", "ring": "solid"}
shape: circle · diamond · square · cross · flag · label. label_pos: above · below · right · left. ring: none · solid · bright — use bright to emphasize state (active, in-solve); dark fills get a ring so they don't vanish on satellite tiles. Shape flag plants a pole+pennant at the position; shape label draws no glyph — the label text itself is the marker.
{"kind": "circle", "id": "rng-210", "lat": 50.385, "lon": 30.061,
"radius_m": 1686.0, "color": "#ff6384", "label": "1.69 km", "fill": false}
radius_m is ground meters — the ring scales with zoom like real terrain. Set fill: true for a translucent area fill (1-sigma estimates). The label pill sits at the top of the ring.
{"kind": "line", "id": "ln-210",
"a": {"lat": 50.401, "lon": 30.061}, "b": {"lat": 50.385, "lon": 30.061},
"color": "#ff6384", "dash": true,
"label": "210 / 1.69km / RSSI -82", "label_at": 0.08}
label_at places the label along the line, 0.0 = at a, 1.0 = at b. Keep it near 0.08 so labels cluster by their anchor instead of overlapping mid-map.
{"kind": "polyline", "id": "est-track",
"points": [[50.4010, 30.0610], [50.4012, 30.0615], [50.4015, 30.0618]],
"color": "#ff8c00", "width": 2.0, "fade": true}
Points ordered old → new, up to 4096. fade: true ramps alpha from faint (old) to solid (new) — the classic breadcrumb look.
{"kind": "pulse", "id": "ping-210-1783270000", "lat": 50.385,
"lon": 30.061, "color": "#ff6384", "duration_s": 1.5}
An expanding, fading ring — one animation per id. Re-sending the same id in your next PUT does not restart it, so full-state republishing stays flicker-free. New event → new id (timestamp suffixes work well). duration_s caps at 5.
{"kind": "arrow", "id": "a1", "lat": 50.385, "lon": 30.061,
"bearing_deg": 235.0, "length_m": 1240.0,
"color": "#00c0ff", "label": "235° · 1.24 km"}
bearing_deg: 0° = north, clockwise. With length_m the shaft spans that ground distance — it scales with zoom and the label renders at the tip, the pointed-at spot. Omit it for a fixed-length screen pointer (bearings-only observations: signal directions, visual sightings).
{"kind": "sector", "id": "trk1", "lat": 50.385, "lon": 30.061,
"bearing_deg": 245.0, "fov_deg": 30.0, "radius_m": 5000.0,
"color": "#00c0ff", "label": "245° · FOV 30° · 5 km"}
A translucent pie slice fov_deg wide (0–360), centered on bearing_deg (0° = north, clockwise), spanning radius_m ground meters — scales with zoom like circles. Built for antenna trackers and directional sensors: publish it live and the operator sees exactly what your dish covers.
The layer id ops is reserved for markers the operator places by hand (right-click the map → Add marker: text label, flag, arrow, or tracker sector). Your tools can read it — GET /api/v1/layers/ops — to see what the operator marked, but PUT and DELETE on it return 403: field annotations belong to the human.
04 / TTL & Staleness
The design assumption is that publishers die: laptops sleep, radios drop, scripts crash. The map must never keep rendering their last words as current truth.
Each item lives for ttl_s (or the layer's default_ttl_s, 5 s if omitted) measured from the PUT that carried it. Expired items drop off the map and out of GET responses.
Every full-layer PUT restamps all items it carries. A publisher looping at 1–5 Hz with default_ttl_s: 5 keeps its layer alive; the moment it stops, the layer drains within seconds.
A layer not updated for 3× its default TTL is flagged: the map's Layers button gains a ! and the popup shows a dimmed stale Ns note — the operator sees the difference between "quiet" and "gone".
Fast-changing values (RSSI halos, estimates) → short TTL, a few seconds. Slow facts the operator should keep seeing through brief dropouts (tower positions) → longer per-item ttl_s, e.g. 30 s, on those items only.
05 / LAN & Auth
Out of the box the API binds 127.0.0.1 — only processes on the GCS machine can publish. For field setups where a second machine publishes over the local network, enable LAN access in Sharing and set a token.
curl -s -X PUT http://gcs-laptop.local:8580/api/v1/layers/beacon \
-H "X-Overlay-Token: field-crew-7" \
-d @layer.json
# without or with a wrong token:
# {"error":"bad or missing X-Overlay-Token"} [401]
The token is a shared secret over plain HTTP — it keeps accidental and casual writers out of your map on a trusted field LAN. It is not a substitute for network security on hostile networks.
The API surface is versioned under /api/v1 — overlay layers are its first module, and future Groundpost releases will add more under the same base without breaking existing publishers.