Chapter 13 — WebSocket API Reference
The Stolo engine exposes a local JSON-over-WebSocket API on ws://127.0.0.1:4848. The app is its primary client, but administrators can use the API for automation, integration, and headless deployments (e.g. scripting a Pi relay).
The API is local-only: the server binds to loopback and every session must authenticate with the per-boot token.
13.1 Conventions
- Requests are JSON objects:
{"action": "<name>", ...params, "request_id": "req_1"}. The optional request_id is echoed on the response, letting clients correlate replies.
- Responses are events:
{"event": "<name>", ...fields, "request_id": "req_1"}.
- Errors:
{"event": "error", "error": "<message>", "request_id": ...}. Unknown actions return Unknown action: <name>.
- Hashes are lowercase hex strings (angle brackets tolerated on input).
- Push events (unsolicited) are delivered to all authenticated clients as they happen.
- Maximum frame size is 16 MB (attachments travel base64-encoded).
- Before setup completes, most actions return
Setup not complete — please complete setup first. While the service is quiesced (deactivated license), mesh-emitting actions return service_quiesced.
13.2 Authentication
The engine writes a random token to <profile>/ws_token (e.g. ~/.gridx/ws_token) at startup. The first message on any connection must be:
{"action": "authenticate", "token": "<contents of ws_token>", "request_id": "req_0"}
Success: {"event": "authenticated"} — followed by a device_identity_ready push. Failure: {"event": "error", "error": "Invalid token"}. Any other action first returns Not authenticated.
13.3 Identity & profile
| Action |
Parameters |
Response event |
get_identity |
— |
identity — identity_hash, destination_hash, display_name, team, role, avatar_color, is_setup_complete |
get_device_identity |
— |
device_identity — device_hash, device_identity_file_path, created_at, is_new |
complete_setup |
display_name (required), team, role, avatar_color |
setup_complete — creates the identity and starts messaging on first run |
set_display_name |
display_name |
display_name_set |
set_avatar_color |
avatar_color |
avatar_color_set |
set_role |
role |
role_set |
install_vendor_cert |
cert_b64 (base64 msgpack [key_id, sig]) |
vendor_cert_installed — key_id, replaced |
13.4 Presence & service control
| Action |
Parameters |
Response event |
announce |
display_name (optional) |
announce_sent — broadcast a presence announce |
set_presence |
presence ("active" or "idle") |
presence_set |
set_service_quiesced |
quiesced (bool) |
service_quiesced — mesh-dark mode on/off |
network_path_changed |
reason (optional, ≤64 chars) |
network_path_ack — triggers a live interface rebuild after a network change |
restart_backend |
— |
Desktop: engine re-execs (clients see backend_restarting). Mobile: returns restart_unavailable (relaunch the app instead). |
13.5 Messaging
| Action |
Parameters |
Response event |
send_message |
destination; content and/or attachment_data (base64) + attachment_type (image|file|audio); optional attachment_name, attachment_duration, delivery_mode (standard|priority|critical) |
message_sent — includes message_id, initial status |
cancel_message |
message_id |
message_cancelled (critical messages in queued/retrying only) |
send_reaction |
recipient, target_message_id, emoji |
reaction result (toggles add/remove) |
get_messages |
peer, limit (50), before |
messages |
get_conversations |
— |
conversations (with hops, has_path enrichment) |
mark_read |
message_id or peer |
marked_read |
delete_message |
message_id |
message_deleted |
delete_conversation |
peer |
conversation_deleted |
get_media |
message_id |
media_data — base64 payload + attachment metadata |
get_peer_signal |
peer |
peer_signal — RSSI/SNR/message count |
Status vocabulary (initial responses and message_status_update pushes): sending, sent, path_requested, queued, retrying, delivered, unconfirmed, failed, failed_permanent, cancelled, too_large, invalid_destination.
13.6 Peers & network
| Action |
Parameters |
Response event |
get_peers |
— |
peers_list — each peer with presence, via_interface, hops, has_path, is_transport, org/vendor fields, signal data |
get_network_status |
— |
network_status — transport_enabled, destination_hash, known_peers, propagation_node, interfaces[], mesh_health, versions |
get_discovered_interfaces |
— |
discovered_interfaces — interfaces published for discovery on the network |
13.7 Groups
| Action |
Parameters |
Response event |
create_group |
name, members[], display_names (optional) |
group_created |
list_groups |
— |
groups |
get_group_members |
group_id, include_left (optional) |
group_members |
send_group_message |
group_id, content, delivery_mode |
group_message_queued — message_id, recipient_count |
get_group_messages |
group_id, limit, before |
group_messages — messages plus per-recipient status rows |
add_group_member / remove_group_member |
group_id, member |
group_member_added / group_member_removed (owner only) |
leave_group |
group_id |
group_left |
mark_group_read |
group_id |
group_marked_read |
13.8 GPS
| Action |
Parameters |
Response event |
update_my_gps |
lat, lon; optional alt, speed, heading, accuracy, timestamp |
gps_updated |
get_all_positions |
max_age (default 600 s) |
positions |
13.9 Radio, RNode & BLE
| Action |
Parameters |
Response event |
get_radio_config |
interface (optional) |
radio_config |
set_radio_region |
region (us|eu|au|asia) |
radio_region_set (applies on restart) |
set_radio_params |
interface (optional); frequency, bandwidth, spreadingfactor, codingrate, txpower |
radio_params_set (restart required) |
scan_all_radios |
— |
all_radios_scan_result — USB + BLE radios found |
ble_scan |
timeout (default 8 s) |
ble_scan_result — devices with is_rnode flags |
ble_switch_config |
target (name or MAC) |
ble_config_switched — sets the radio port to ble://<target> |
ble_enable / ble_disable / ble_start_pairing |
port (optional) |
ble_enabled / ble_disabled / ble_pairing_started (with PIN when available) |
13.10 Interfaces & engine configuration
| Action |
Parameters |
Response event |
add_interface |
name, type (TCPClientInterface|TCPServerInterface|BackboneInterface|RNodeInterface|AutoInterface) + type-specific params (TCP client: target_host, target_port; TCP server: listen_port, optional listen_ip; Backbone: listen_on+port or remote+target_port; RNode: port + radio params) |
interface_added (restart to activate) |
remove_interface |
name |
interface_removed |
rename_interface |
old_name, new_name |
interface_renamed |
set_interface_enabled |
name, enabled |
interface_enabled_set |
set_interface_mode |
interface, mode (full|gateway|access_point|roaming|boundary) |
interface_mode_set |
get_ifac_config / set_ifac_config / remove_ifac |
interface or apply_all; network_name, passphrase, ifac_size (8–512) |
ifac_config / ifac_config_set / ifac_removed |
get_discovery_config / set_discovery_config |
name + discovery keys (discoverable, discovery_name, reachable_on, announce_interval, discovery_stamp_value, discovery_encrypt, publish_ifac, bootstrap_only, latitude, longitude, height) |
discovery_config / discovery_config_set |
get_reticulum_config |
— |
reticulum_config — enable_transport, network_identity, interface_discovery_sources, primary_publisher_org_id, strict_mode |
set_reticulum_config |
enable_transport (bool), network_identity (path or "") |
reticulum_config_set |
13.11 Trust, quarantine, blocking & organizations
| Action |
Parameters |
Response event |
list_quarantined |
limit (100), before |
quarantined |
accept_from_quarantine |
message_id, trust_target (null|"sender"|"organization") |
quarantine_accepted — message replayed into the inbox |
deny_from_quarantine |
message_id |
quarantine_denied |
deny_and_block_from_quarantine |
message_id, duration_seconds or until (optional) |
quarantine_denied_and_blocked |
list_trusted_peers / remove_trusted_peer |
— / source_hash |
trusted_peers_list / trusted_peer_removed |
block_peer |
source_hash; duration_seconds (relative) or until (absolute); omit both = permanent |
peer_blocked (or error identity_unknown) |
unblock_peer |
identity_hash |
peer_unblocked |
list_blocked |
— |
blocked_peers_list |
set_strict_mode |
enabled |
strict_mode_set |
create_org |
name, mode (generate|import_hash|import_keyset), hash, keyset_b64, overwrite |
org_created |
list_orgs / delete_org |
— / org_id, delete_keyset_file |
orgs / org_deleted |
set_primary_publisher |
org_id (or null to clear) |
primary_publisher_set (restart required) |
set_org_trusted |
org_id, trusted |
org_trust_set |
13.12 Push events
Delivered asynchronously to all authenticated clients:
| Event |
Fired when |
device_identity_ready |
On authentication / after setup — device_hash, is_new |
new_message / message_received |
An inbound message is delivered — sender, content, attachment metadata, signal data |
message_status_update |
Any outbound message changes status — message_id, status, optional rtt, retry_count, via_interface |
reaction_update |
A reaction is added/removed on a message |
peer_discovered |
A new peer announce is heard — full peer record |
peer_presence_changed |
A peer's presence tier changes (2 active / 1 idle / 0 offline) |
peer_gps_updated |
A peer shares a position |
peer_org_verified / peer_vendor_verified |
A peer's org membership / vendor certificate verifies |
quarantine_added / quarantine_purged |
A message is quarantined / expired entries are purged |
mesh_health |
Mesh health transitions (healthy / degraded, with reasons) |
interface_*, group_* |
Interface config changes; group lifecycle (created, invite received, message received/sent, member added/removed/left, renamed, recipient status) |
backend_restarting |
The engine is about to restart |
13.13 Example session
import asyncio, json, pathlib, websockets
async def main():
token = (pathlib.Path.home() / ".gridx" / "ws_token").read_text().strip()
async with websockets.connect("ws://127.0.0.1:4848") as ws:
await ws.send(json.dumps({"action": "authenticate", "token": token, "request_id": "r0"}))
print(await ws.recv()) # {"event": "authenticated", ...}
await ws.send(json.dumps({"action": "get_network_status", "request_id": "r1"}))
print(await ws.recv())
await ws.send(json.dumps({
"action": "send_message", "request_id": "r2",
"destination": "<peer destination hash>",
"content": "Radio check", "delivery_mode": "priority",
}))
# Watch for message_sent, then message_status_update pushes.
while True:
print(await ws.recv())
asyncio.run(main())
A ready-made interactive client ships with the engine as backend/test_client.py.