Browse all 16 endpoints
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
errorfield. 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.
*_ustimestamps andsinceuse 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
idandstorage_idare database row IDs. Use these for thepacket_idquery/path parameters on packet, observation, count, and traceroute endpoints. A response field namedpacket_idon 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'sidis a separate string identifier. - Use decimal node IDs throughout. The packet sender/recipient filters and the QR and coverage paths also accept Python-style
0xhexadecimal 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/chatroute. Do not treat a decodedpayloadstring as trusted HTML.
Quick examples
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
{
"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
{
"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.
sinceis strictly greater than the supplied timestamp. Storelatest_import_timefor the next poll and deduplicate byid. 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_timeis returned only when the list response has a positive import timestamp; it is absent for empty results and single-packet lookups.packet_idlookup returns{"packets":[]}with HTTP 200 when missing. Its objects omitto_long_nameandreply_id; nullableportnumis possible in this mode.containsis case-insensitive. Withportnum=1, matching is performed on decoded displayed payload text (including redaction placeholders) after the database limit is applied, so fewer thanlimitresults 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
limitfalls back to 50. Invalidsince,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):hourorday. Default:hour.length(optional, int): Number of periods to look back. Default:24.
Response Example
{
"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):hourorday. 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 combinedsentandseentotals for that node.
Response Example (series)
{
"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)
{
"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):hourorday.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
{
"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):hourorday. 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, max100.offset(optional, int): Pagination offset. Default:0.
Response Example
{
"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):tracerouteorneighbor. If omitted, returns both.node_id(optional, int): Filter edges to only those touching a node.
Response Example
{
"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
{"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:
[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
{
"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 oren.section(optional, string): Return only one section (e.g.,nodelist,firehose).
Response Example
{
"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
{
"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
{
"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
{
"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
{
"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 or0xhexadecimal form.
Response Example
{
"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 or0xhexadecimal form.
Query Parameters
mode(string, defaultperimeter):perimeterorheatmap. Currently every value other than the exact stringperimeterselects the heatmap branch.freq_mhz(number, default907): Frequency in MHz; range 1-10000.tx_dbm(number, default20): Transmit power in dBm; range -50 to 60.tx_height_m(number, default5): Transmit antenna height in meters; range 0-1000.rx_height_m(number, default1.5): Receive antenna height in meters; range 0-1000.radius_km(number, default40): Maximum distance in kilometers; range 1-200.step_km(number, default0.25): Radial sample spacing in kilometers; range 0.05-50.reliability(number, default0.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)
{
"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)
{
"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.