Files
meshview/docs/API_Documentation.md
T

741 lines
25 KiB
Markdown

# API Documentation
Base URL: `http(s)://<host>`
This reference covers all 16 GET routes registered in `meshview/web_api/api.py`,
including `/health` and `/version`. The browser version is served at `/api.html`.
Responses below are illustrative examples; actual values depend on stored data.
Regenerate the browser reference after editing this file with
`node scripts/build-api-docs.mjs`.
## Conventions
- Successful API responses are JSON. Explicit validation errors usually return JSON
with an `error` field. Some unhandled errors return HTTP 500 with a plain-text
body; check HTTP status and Content-Type before parsing an error response.
- All routes are read-only. The application does not require an API token; a reverse
proxy may impose authentication. There are no application-level write endpoints.
- `*_us` timestamps and `since` use microseconds since the Unix epoch. Snapshot dates
and statistics buckets use UTC. Node latitude/longitude integers must be divided
by 10,000,000 to obtain degrees; coverage coordinates are already degrees.
- Packet `id` and `storage_id` are database row IDs. Use these for the `packet_id`
query/path parameters on packet, observation, count, and traceroute endpoints.
A response field named `packet_id` on a packet object is instead its over-the-air
Meshtastic ID. `reply_id`, when present, is also an over-the-air ID. These ID types
are not interchangeable. Node IDs are numeric Meshtastic node numbers; the node
object's `id` is a separate string identifier.
- Use decimal node IDs throughout. The packet sender/recipient filters and the QR
and coverage paths also accept Python-style `0x` hexadecimal numbers. This is
not supported consistently by the other routes.
- Public broadcast text is readable. Direct text messages on port 1 are returned as
`<redacted>`. Request chat with `/api/packets?portnum=1`; there is no `/api/chat`
route. Do not treat a decoded `payload` string as trusted HTML.
## Quick examples
```sh
curl 'http://localhost:8081/api/nodes?days_active=3'
curl 'http://localhost:8081/api/packets?portnum=1&limit=100'
curl 'http://localhost:8081/api/stats/count'
curl 'http://localhost:8081/api/coverage/1234?mode=perimeter&radius_km=20'
```
## 1. Nodes API
### GET `/api/nodes`
Returns a list of nodes, with optional filtering.
Query Parameters
- `node_id` (optional, int): Exact node ID.
- `role` (optional, string): Node role.
- `channel` (optional, string): Channel name.
- `hw_model` (optional, string): Hardware model.
- `days_active` (optional, int): Nodes seen within the last N days. Omit for all
nodes with a non-null last-seen timestamp; invalid integers are ignored.
Nodes are sorted by `short_name` ascending. `role` is normalized to uppercase;
channel and hardware filters are exact matches. No pagination is implemented.
Errors: HTTP 500 with `{"error":"Failed to fetch nodes"}` for handler failures;
the storage helper may return an empty list for database failures.
Response Example
```json
{
"nodes": [
{
"id": "!000004d2",
"node_id": 1234,
"long_name": "Alice",
"short_name": "A",
"hw_model": "T-Beam",
"firmware": "1.2.3",
"role": "client",
"last_lat": 377749000,
"last_long": -1224194000,
"channel": "main",
"is_mqtt_gateway": true,
"first_seen_us": 1736283723456789,
"last_seen_us": 1736370123456789
}
]
}
```
---
## 2. Packets API
### GET `/api/packets`
Returns packets with optional filters.
Query Parameters
- `packet_id` (optional, decimal int): Return one packet by database row ID (overrides other filters).
- `limit` (optional, int): Max packets to return, clamped 1-1000. Default: `50`.
- `since` (optional, int): Only packets imported after this microsecond timestamp.
- `portnum` (optional, int): Filter by port number.
- `contains` (optional, string): Payload substring filter.
- `from_node_id` (optional, int): Filter by sender node ID.
- `to_node_id` (optional, int): Filter by recipient node ID.
- `node_id` (optional, int): Legacy filter matching either from or to node ID.
Response Example
```json
{
"packets": [
{
"id": 123,
"storage_id": 123,
"packet_id": 3314808102,
"import_time_us": 1736370123456789,
"channel": "main",
"from_node_id": 5678,
"to_node_id": 4294967295,
"portnum": 1,
"long_name": "Alice",
"payload": "Hello, mesh!",
"to_long_name": null,
"reply_id": 122
}
],
"latest_import_time": 1736370123456789
}
```
Notes
- For `portnum=1` (text messages), packets are filtered to remove sequence-only payloads.
- Results are newest first. `since` is strictly greater than the supplied timestamp.
Store `latest_import_time` for the next poll and deduplicate by `id`. A bounded
result can omit older records if more packets arrive than the requested limit;
this endpoint is not a durable cursor or backfill API.
- `latest_import_time` is returned only when the list response has a positive import
timestamp; it is absent for empty results and single-packet lookups.
- `packet_id` lookup returns `{"packets":[]}` with HTTP 200 when missing. Its objects
omit `to_long_name` and `reply_id`; nullable `portnum` is possible in this mode.
- `contains` is case-insensitive. With `portnum=1`, matching is performed on decoded
displayed payload text (including redaction placeholders) after the database limit is applied, so fewer than `limit` results
may be returned. For other requests, it searches stored payload bytes cast to text;
this is not a general search over decoded protobuf fields.
- Invalid `limit` falls back to 50. Invalid `since`, `portnum`, and node filters are
logged and ignored. Invalid single-packet IDs return HTTP 400; handler failures
return HTTP 500 with `{"error":"Failed to fetch packets"}`.
---
## 3. Channels API
### GET `/api/channels`
Returns channels seen in a time period.
Query Parameters
- `period_type` (optional, string): `hour` or `day`. Default: `hour`.
- `length` (optional, int): Number of periods to look back. Default: `24`.
Response Example
```json
{
"channels": ["LongFast", "MediumFast", "ShortFast"]
}
```
---
Errors: storage/period failures return HTTP 200 with `channels: []` and an `error`
field. Non-integer `length` is parsed outside that handler and can return HTTP 500.
Use a positive integer length and the literal `hour` or `day`.
## 4. Stats API
### GET `/api/stats`
Returns packet statistics aggregated by time periods, with optional filtering.
Query Parameters
- `period_type` (optional, string): `hour` or `day`. Default: `hour`.
- `length` (optional, int): Number of periods to include. Default: `24`.
- `channel` (optional, string): Filter by channel (case-insensitive).
- `portnum` (optional, int): Filter by port number.
- `to_node` (optional, int): Filter by destination node ID.
- `from_node` (optional, int): Filter by source node ID.
- `node` (optional, int): If provided, return combined `sent` and `seen` totals for that node.
Response Example (series)
```json
{
"period_type": "hour",
"length": 24,
"channel": "LongFast",
"portnum": 1,
"to_node": 12345678,
"from_node": 87654321,
"data": [
{ "period": "2025-08-08 14:00", "count": 10 },
{ "period": "2025-08-08 15:00", "count": 7 }
]
}
```
Response Example (`node` totals)
```json
{
"node_id": 12345678,
"period_type": "hour",
"length": 24,
"sent": 0,
"seen": 0
}
```
---
The series response contains only buckets with records, ordered oldest first;
missing hours/days are not filled with zeroes. Periods are UTC strings.
Current `node`-mode limitation: the handler reads a `total` key that the statistics
helper does not provide, so `sent` and `seen` currently return zero. It also uses
recipient filtering for the second query, rather than gateway observations. This
mode ignores the other filters. For usable sent/observation totals, request
`/api/stats/count?from_node=1234&period_type=day&length=1` instead.
Errors: HTTP 400 for an unsupported `period_type`, non-integer `length`, `node`,
`portnum`, `from_node`, or `to_node`. Storage failures can return HTTP 500.
Use a positive `length`; the handler does not impose a positive lower bound.
### GET `/api/stats/count`
Returns total packet counts, optionally filtered.
Query Parameters
- `packet_id` (optional, int): Filter packet_seen by packet ID.
- `period_type` (optional, string): `hour` or `day`.
- `length` (optional, int): Number of periods to include.
- `channel` (optional, string): Filter by channel.
- `from_node` (optional, int): Filter by source node ID.
- `to_node` (optional, int): Filter by destination node ID.
Response Example
```json
{
"total_packets": 12345,
"total_seen": 67890
}
```
---
Without filters, counts cover the whole retained database. With time, channel,
or direction filters, omitted time settings default to `period_type=day&length=1`.
`total_seen` counts observation rows for packets matching the packet import-time
window, not distinct gateways. Channel matching is case-insensitive.
`packet_id` applies only to `total_seen` and overrides its other filters: it counts
all observations of that database packet ID. It does not narrow `total_packets`.
In particular, `packet_id` alone leaves `total_packets` as the global packet total.
Errors: HTTP 400 for non-integer ID/length filters. Unsupported `period_type` or
storage failures can return HTTP 500; use `hour` or `day` and a positive length.
### GET `/api/stats/top`
Returns nodes sorted by packets seen, with pagination.
Query Parameters
- `period_type` (optional, string): `hour` or `day`. Default: `day`.
- `length` (optional, int): Number of periods to include. Default: `1`.
- `channel` (optional, string): Filter by channel.
- `limit` (optional, int): Max nodes to return. Default: `20`, max `100`.
- `offset` (optional, int): Pagination offset. Default: `0`.
Response Example
```json
{
"total": 250,
"limit": 20,
"offset": 0,
"nodes": [
{
"node_id": 1234,
"long_name": "Alice",
"short_name": "A",
"channel": "main",
"sent": 100,
"seen": 240,
"avg": 2.4
}
]
}
```
---
`sent` counts packet records from each node; `seen` counts observation records for
that sender's packets. `avg` is `round(seen / max(sent, 1), 2)`, not a distinct-gateway
count. Windows are anchored to the newest packet and newest observation in the
respective tables, not wall-clock time. Nodes with zero activity are included;
`total` counts nodes matching the channel filter, not just active nodes.
Channel matching is exact. Use positive `length` and `limit`, and a nonnegative
`offset`. Only the upper limit of 100 is enforced. Invalid numeric strings can
return HTTP 500; any `period_type` other than the literal `hour` is treated as days.
## 5. Edges API
### GET `/api/edges`
Returns network edges (connections between nodes) based on traceroutes and neighbor info.
Traceroute edges are collected over the last 12 hours. Neighbor edges are based on
port 71 packets.
A completed traceroute is observed as a response packet, whose `from`/`to` are reversed
with respect to the traced path, so edges are built as `to -> route[] -> from`. `route[]`
may contain `0xFFFFFFFF` placeholders for hops the mesh could not identify; the edges on
either side of such a placeholder are omitted, because the nodes around an unknown hop
are not neighbours.
Query Parameters
- `type` (optional, string): `traceroute` or `neighbor`. If omitted, returns both.
- `node_id` (optional, int): Filter edges to only those touching a node.
Response Example
```json
{
"edges": [
{ "from": 12345678, "to": 87654321, "type": "traceroute" },
{ "from": 11111111, "to": 22222222, "type": "neighbor" }
]
}
```
---
Unknown `type` values return an empty edge list. Invalid `node_id` returns HTTP 400;
storage failures can return HTTP 500. A missing node is not an error and normally
returns an empty list. Neighbor edges come from the latest stored port-71 sample
(the storage helper defaults to 50 packets), not the traceroute 12-hour window.
## 6. Daily Snapshots and Configuration
### GET `/api/snapshots/daily`
Returns stored daily snapshots. For new snapshots, `packet_count` is the number of
packet records imported during `date` (00:00 UTC inclusive to the next midnight
exclusive). The scheduled capture records the previous complete UTC day. Repeated
observations of the same packet are not counted separately. `node_count` counts distinct
packet senders whose packets were imported or observed during that UTC day (including
observations of previously imported packets). `gateway_count` counts distinct MQTT
gateways with packet observations imported during that day. These are activity counts,
not database totals; recipients alone do not count as active nodes. Activity uses local
import timestamps, not device timestamps. Older snapshots retain their originally stored
counts and are not automatically converted. Counts depend on the packet and observation
history still retained at capture time.
Query Parameters
- `length` (optional, int): Return the latest N snapshot records, clamped to 1-3650.
Omit for all stored snapshots. Results are always ordered by date ascending.
This is a record limit, not a guarantee of consecutive calendar dates.
Response Example
```json
{"data":[{"date":"2026-10-24","node_count":143,"packet_count":28465,"gateway_count":17,"captured_at_us":1792886400000000}]}
```
No stored snapshots returns `{"data":[]}`. Invalid `length` returns HTTP 400;
storage failures return HTTP 500. `captured_at_us` is the capture time, which may
be later than the day summarized by `date`.
### GET `/api/config`
Returns a safe subset of server configuration.
`site.map_channel_colors` contains channel color overrides from the optional
`[map_channel_colors]` INI section. Each entry is `channel = #RRGGBB`; invalid colors
are omitted. Channel matching on the map ignores case, spaces, hyphens, and underscores.
Unlisted channels use generated colors. For example:
```ini
[map_channel_colors]
LongFast = #4363d8
LocalChat = #18aa18
```
No query parameters are supported. Only the `site`, MQTT server/topics, and cleanup
fields shown here are exposed; credentials and the database connection string are
not returned. Feature flags and cleanup booleans are lowercase strings (`"true"` /
`"false"`), not JSON booleans. Cleanup scheduling values are strings; map bounds are
numbers and refresh intervals are integers. The removed `starting` option is not
returned: `/` always serves the dashboard. Handler failures return HTTP 500.
Restart the web server and reload the map after changing these settings. Omitting
the section uses generated colors for every channel.
Response Example
```json
{
"site": {
"domain": "example.com",
"language": "en",
"title": "Meshview",
"message": "",
"nodes": "true",
"chat": "true",
"everything": "true",
"graphs": "true",
"stats": "true",
"net": "true",
"map": "true",
"top": "true",
"map_top_left_lat": 39.0,
"map_top_left_lon": -123.0,
"map_bottom_right_lat": 36.0,
"map_bottom_right_lon": -121.0,
"map_interval": 3,
"map_channel_colors": {"LongFast":"#4363d8"},
"firehose_interval": 3,
"weekly_net_message": "Weekly Mesh check-in message.",
"net_tag": "#BayMeshNet",
"version": "3.0.0"
},
"mqtt": {
"server": "mqtt.example.com",
"topics": ["msh/region/#"]
},
"cleanup": {
"enabled": "false",
"days_to_keep": "14",
"hour": "2",
"minute": "0",
"vacuum": "false"
}
}
```
---
## 7. Language API
### GET `/api/lang`
Returns translation strings.
Query Parameters
- `lang` (optional, string): Language code (e.g., `en`, `es`). Default from config or `en`.
- `section` (optional, string): Return only one section (e.g., `nodelist`, `firehose`).
Response Example
```json
{
"title": "Meshview",
"search_placeholder": "Search..."
}
```
---
Unknown or invalid language codes fall back to the English file. An unknown section
returns HTTP 404. The response has no wrapper: it is either the full translation
object or the requested section. Translation-file failures can return HTTP 500.
## 8. Packets Seen API
### GET `/api/packets_seen/{packet_id}`
Returns packet_seen entries for a packet.
Path Parameters
- `packet_id` (required, int): Packet ID.
Response Example
```json
{
"seen": [
{
"packet_id": 123,
"node_id": 456,
"rx_time": 1753188300,
"hop_limit": 7,
"hop_start": 0,
"channel": "main",
"rx_snr": 5.0,
"rx_rssi": -90,
"topic": "msh/region/#",
"import_time_us": 1736370123456789
}
]
}
```
---
`node_id` identifies the observing MQTT gateway. `rx_time` is the device-provided
receive time in Unix seconds, while `import_time_us` is local import time
in microseconds. Signal fields can be null. Multiple rows can describe the same
gateway; consumers wanting unique gateways must deduplicate them. Hop count is
usually `max(0, hop_start - hop_limit)` when both values are known. Observations
do not by themselves prove a direct radio link from the sender to that gateway.
No observations, including for an unknown packet, returns `{"seen":[]}` with HTTP
200. Invalid path IDs return HTTP 400; handler failures return HTTP 500.
## 9. Traceroute API
### GET `/api/traceroute/{packet_id}`
Returns traceroute details and derived paths for a packet.
Path Parameters
- `packet_id` (required, int): Packet ID.
A completed traceroute is observed as a response packet, whose `from`/`to` are reversed
with respect to the traced path. `initiator` and `target` resolve this: the forward path
runs `initiator -> forward_hops -> target` and the return path runs
`target -> reverse_hops -> initiator`, in both directions of travel. `forward_hops` and
`reverse_hops` contain only the intermediate hops, never the endpoints.
A gateway usually reports a response that is still travelling home, so `reverse_hops` is
often incomplete. `reverse_complete` says whether that observation had reached the
initiator; only then does the corresponding `winning_paths.reverse` entry end at the
initiator.
`winning_paths` is built from response observations, since a response proves the request
reached the target. `complete` on a `unique_forward_paths` entry says whether some
response reported that hop list, and so whether it is known to end at the target.
Response Example
```json
{
"packet": {
"id": 123,
"from": 222,
"to": 111,
"initiator": 111,
"target": 222,
"channel": "main"
},
"traceroute_packets": [
{
"index": 0,
"gateway_node_id": 333,
"done": true,
"forward_hops": [444, 555],
"reverse_hops": [555, 444],
"reverse_complete": true
}
],
"unique_forward_paths": [
{ "path": [444, 555], "count": 2, "complete": true }
],
"unique_reverse_paths": [
[555, 444]
],
"winning_paths": {
"forward": [[111, 444, 555, 222]],
"reverse": [[222, 555, 444, 111]]
}
}
```
---
`traceroute_packets` can be empty even when the packet exists. Unknown database
packet IDs return HTTP 404. Invalid integer IDs and storage failures can return
HTTP 500; this handler does not implement a separate HTTP 400 validation branch.
## 10. Health API
### GET `/health`
Returns service health and database status.
Response Example
```json
{
"status": "healthy",
"timestamp": "2025-07-22T12:45:00+00:00",
"version": "3.0.3",
"git_revision": "abc1234",
"cleanup": {
"enabled": true,
"days_to_keep": 14,
"scheduled_time": "02:00",
"vacuum": false,
"status": "ok",
"status_file": "dbcleanup.status.json",
"last_run": {
"status": "ok",
"started_at": "2026-06-04T09:00:00+00:00",
"completed_at": "2026-06-04T09:00:03+00:00",
"cutoff_at": "2026-05-21T09:00:00+00:00",
"days_to_keep": 14,
"vacuum_requested": false,
"vacuum_completed": false,
"rows_deleted": {
"packet": 1200,
"packet_seen": 3400,
"traceroute": 42,
"node": 3
},
"error": null
}
},
"backup": {
"enabled": true,
"backup_dir": "./backups",
"scheduled_time": "02:00",
"status": "ok",
"status_file": "dbbackup.status.json",
"last_run": {
"status": "ok",
"started_at": "2026-06-04T09:00:00+00:00",
"completed_at": "2026-06-04T09:00:02+00:00",
"backup_dir": "./backups",
"database_path": "packets.db",
"backup_file": "backups/packets_backup_20260604_090000.db.gz",
"original_size_bytes": 12939444,
"compressed_size_bytes": 4211560,
"compression_percent": 67.5,
"error": null
}
},
"database": "connected",
"database_size": "12.34 MB",
"database_size_bytes": 12939444
}
```
---
No query parameters. HTTP 200 reports `status: "healthy"` with database connectivity.
A database failure returns HTTP 503 with `status: "unhealthy"` and
`database: "disconnected"`. `seconds_since_last_message` is included only when a
latest imported packet exists; SQLite size fields appear only when available.
Cleanup and backup reports include configured schedules and status-file contents
when present. Their status may be `disabled`, `unknown`, `error`, or the last job's
recorded status; a failed backup/cleanup does not itself change the top-level HTTP
status. The optional `last_run` objects depend on the job's stored report.
## 11. Version API
### GET `/version`
Returns version metadata.
Response Example
```json
{
"version": "3.0.3",
"release_date": "2026-1-15",
"git_revision": "abc1234",
"git_revision_short": "abc1234"
}
```
No query parameters. HTTP 500 with an `error` field if version metadata cannot be
retrieved. A revision may be `unknown` if Git metadata is unavailable.
## 12. Node Contact URL
### GET `/api/node/{node_id}/qr`
Returns a Meshtastic shared-contact URL as JSON, not a PNG or SVG image. The client
can encode `meshtastic_url` in a QR code. No query parameters are supported.
Path Parameters
- `node_id` (required, int): Node number in decimal or `0x` hexadecimal form.
Response Example
```json
{
"node_id":1234,
"long_name":"Alice",
"short_name":"A",
"role":"CLIENT",
"meshtastic_url":"https://meshtastic.org/v/#BASE64URL_CONTACT"
}
```
The example contact payload is a placeholder, not an importable contact. The URL
encodes node identity and available user metadata as a base64url SharedContact
protobuf, with `manually_verified` false. Metadata can be null.
Errors: HTTP 400 for invalid IDs; HTTP 404 when the node is absent; HTTP 500 for
contact serialization failures or unhandled storage errors.
## 13. Predicted Radio Coverage
### GET `/api/coverage/{node_id}`
Computes an ITM area-model prediction centered on the node's last stored position.
This is a model prediction, not a measured coverage boundary or terrain profile.
The optional `pyitm` dependency is required. Calculations run off the event loop
and reuse cached model results. All parameters are optional except the node ID.
Path Parameters
- `node_id` (required, int): Node number in decimal or `0x` hexadecimal form.
Query Parameters
- `mode` (string, default `perimeter`): `perimeter` or `heatmap`. Currently every
value other than the exact string `perimeter` selects the heatmap branch.
- `freq_mhz` (number, default `907`): Frequency in MHz; range 1-10000.
- `tx_dbm` (number, default `20`): Transmit power in dBm; range -50 to 60.
- `tx_height_m` (number, default `5`): Transmit antenna height in meters; range 0-1000.
- `rx_height_m` (number, default `1.5`): Receive antenna height in meters; range 0-1000.
- `radius_km` (number, default `40`): Maximum distance in kilometers; range 1-200.
- `step_km` (number, default `0.25`): Radial sample spacing in kilometers; range 0.05-50.
- `reliability` (number, default `0.5`): ITM time fraction; range 0.01-0.99.
- `threshold_dbm` (number, default `-120`): Perimeter received-power threshold in dBm;
range -200 to 0. Ignored by heatmap computation, although still validated.
Numeric bounds are inclusive, values must be finite, and `radius_km / step_km`
must not exceed 4000. Smaller steps increase computation cost.
Response Example (`mode=perimeter`)
```json
{
"mode":"perimeter",
"threshold_dbm":-120.0,
"perimeter":[[30.21,-93.20],[30.209,-93.198]]
}
```
`perimeter` contains `[latitude, longitude]` degree pairs for the furthest qualifying
sample on each modeled bearing. Bearings with no qualifying sample are omitted;
the list is not necessarily a closed GeoJSON polygon.
Response Example (`mode=heatmap`)
```json
{
"mode":"heatmap",
"min_dbm":-130.0,
"max_dbm":-80.0,
"points":[[30.21,-93.20,-110.5]]
}
```
`points` contains `[latitude, longitude, received_power_dbm]` triples. Min/max values
provide a color-scale range, extended from the defaults to include computed values.
An empty perimeter or point list is a valid result when no samples qualify.
Errors: HTTP 400 for invalid IDs, nonnumeric/nonfinite/out-of-range parameters, or
an excessive radius/step ratio; HTTP 404 for an unknown node or missing/zero-valued
stored coordinates; HTTP 503 when `pyitm` is unavailable. Dependency availability is checked
before numeric query validation and node lookup. Unexpected model/storage failures
can return HTTP 500.