API v1

Draw on the map.

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

Sixty seconds to first marker

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.

1 · Check it's alive
curl -s http://127.0.0.1:8580/api/v1/ping
# {"ok":true,"v":1}
2 · Publish a layer
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.

3 · Update, then clean up
# 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

Five verbs, one resource

MethodPathPurpose
GET/api/v1/pingLiveness + API version
GET/api/v1/layersList 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
Layer ids

Lowercase [a-z0-9_-], 1–32 chars. One id per publisher is the usual pattern (beacon, scanner, …).

Limits & errors

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

Six primitives

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.

marker a point of interest
{"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.

circle range ring / uncertainty area
{"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.

line labeled connection between two points
{"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.

polyline track / breadcrumb trail
{"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.

pulse one-shot event animation
{"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.

arrow directional pointer
{"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).

sector antenna coverage wedge
{"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.

Groundpost map with operator markers: a ground-length arrow labeled 253 degrees 500 meters, an orange flag, and a text label
Operator markers on satellite imagery — ground-length arrow (bearing · distance at the tip), flag, text label.
The ops layer

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

Stale data never looks live

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.

Per-item expiry

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.

Replace refreshes

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.

Visible staleness

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".

Pick TTLs by meaning, not by rate

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

Local by default, LAN when you need it

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.

With a token configured
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.