API Reference¶
The same web server that serves the mobile UI exposes a small read-mostly REST API and one WebSocket. It is LAN-only, has no authentication, and runs on port 8080.
http://<desktop-ip>:8080/api/...
ws://<desktop-ip>:8080/ws/tracking
Errors use FastAPI's shape: {"detail": "<message>"} with a 4xx/5xx status
(404 unknown satellite, 422 bad datetime, 503 when the engine/rig isn't
ready, 409 when a rig action needs a connection).
Server source: src/web/app.py.
REST endpoints¶
Satellites¶
GET /api/satellites — optional ?group= (amateur, weather, …,
operational, or fav:<id>).
[
{
"norad_cat_id": 25544,
"name": "ISS (ZARYA)",
"alt_names": ["ZARYA"],
"status": "alive",
"updated_at": "2026-05-10T09:00:00Z",
"amsat_status": "operational",
"favorite_group": 0,
"tle_no_result_since": null
}
]
GET /api/favorites — same shape, every satellite in any custom group.
GET /api/custom-groups — [{ "id": 1, "name": "Favorite 1", "sort_order": 0 }]
PUT /api/satellites/{norad}/favorite-group — body { "group_id": 1 }
(0 clears). 204.
Transmitters¶
GET /api/satellites/{norad}/transmitters
[
{
"uuid": "…", "norad_cat_id": 25544, "description": "Voice Repeater",
"type": "Transponder",
"downlink_low": 145800000, "downlink_high": 145800000,
"uplink_low": 437800000, "uplink_high": 437800000,
"mode": "FM", "invert": false, "baud": null,
"ctcss_tone": 67.0, "ctcss_tone_type": "…",
"alive": true, "source": "satnogs", "manual_override": false, "notes": ""
}
]
Frequencies are in Hz. downlink_low/downlink_high differ for a
transponder passband and are equal for a fixed channel.
Passes¶
GET /api/satellites/{norad}/passes
| Query | Default | Notes |
|---|---|---|
hours |
24 |
prediction window, 0 < hours ≤ 168 |
min_el |
5 |
minimum max-elevation, degrees |
track |
false |
include 15-second (az, el) samples per pass |
from_dt, to_dt |
— | ISO 8601; overrides hours |
[
{
"norad_cat_id": 25544,
"aos": "2026-05-10T12:45:00+00:00",
"tca": "2026-05-10T12:53:00+00:00",
"los": "2026-05-10T13:01:00+00:00",
"max_elevation_deg": 72.4,
"max_elevation_time": "2026-05-10T12:53:00+00:00",
"aos_azimuth_deg": 201.0,
"los_azimuth_deg": 22.0,
"duration_s": 960.0,
"duration_seconds": 960.0,
"quality": "excellent",
"track_points": null
}
]
quality is excellent (≥60°) / good (≥30°) / fair (≥10°) / low.
GET /api/passes/group — ?group= (default amateur), from_dt, to_dt,
min_el. Each item adds sat_name and amsat_status and omits the azimuth
detail.
TLE, status, location¶
GET /api/tle/status — per-satellite quality_score, epoch,
fetched_at, source.
GET /api/status — { "version", "status", "satellite_count", "tle_count", "uptime_s" }
GET /api/amsat — { "<designator>": "<status>" } map.
GET /api/location — observer QTH (latitude_deg, longitude_deg,
elevation_m, source, city, country, status_text). 404 if unset.
POST /api/location/browser — body { "latitude", "longitude",
"accuracy_m"?, "elevation_m"? }; stores a browser-geolocation fix.
Rig / rotator (mobile Antenna tab)¶
All return 204; 503 if unavailable, 409 if not connected.
POST /api/rig/toggle— toggle Doppler correctionPOST /api/rot/toggle— toggle rotator trackingPOST /api/rig/connect— body{ "norad": 25544, "xpdr_uuid": "…" }POST /api/rig/disconnect
WebSocket¶
ws://<host>:8080/ws/tracking?norad=<id> (defaults to 25544). One JSON
message per second for that one satellite:
{
"norad": 25544,
"timestamp": "2026-05-10T12:00:00+00:00",
"elevation_deg": 45.12,
"azimuth_deg": 180.34,
"range_km": 412.5,
"range_rate_km_s": -2.134567,
"is_above_horizon": true
}
If the engine isn't configured or the TLE is missing, the payload is
{ "norad": <id>, "error": "<message>" }.
Example¶
# ISS passes in the next 3 days above 20°
curl "http://localhost:8080/api/satellites/25544/passes?hours=72&min_el=20" | jq '.[].aos'
import websockets, asyncio, json
async def main():
url = "ws://localhost:8080/ws/tracking?norad=25544"
async with websockets.connect(url) as ws:
while True:
print(json.loads(await ws.recv()))
asyncio.run(main())