Reticulum sidecar IPC contract
HTTP + WebSocket on 127.0.0.1 (ephemeral port in production; default dev port 19437).
Aligned with Ratspeak ratspeak-tauri commands — not meshchat aiohttp.
Electron main validates proxy paths: must start with /api/v1/ (no .. segments).
REST
Status and app
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/status |
{ status, version, rns_ready, lxmf_ready } — see listen-first note below |
|
| GET | /api/v1/app/info |
{ sidecar_version, rns_version?, lxmf_version? } |
Listen-first / ready flags: status: "ok" means the HTTP server is accepting connections (Electron health poll succeeds). Cold start clears persisted ready bits; rns_ready / lxmf_ready stay false until attach_live finishes (RNS/LXMF bridge up). TCP/API clients can hit /api/v1/status and identity routes while live attach (path table, BLE Peer, PN messagestore) continues in the background.
Identity
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/identity/status |
{ configured, identity_hash, lxmf_hash, display_name?, public_key? } |
|
| POST | /api/v1/identity/register-known |
{ destination_hash, public_key } |
{ ok } (registers 64-byte pubkey for Direct LXMF / Columba QR) |
| POST | /api/v1/identity/generate |
{ display_name?, replace? } |
{ ok, mnemonic?, identity_hash, lxmf_hash } |
| POST | /api/v1/identity/import |
{ mnemonic, display_name?, replace? } |
{ ok, identity_hash, lxmf_hash } |
| POST | /api/v1/identity/import-backup |
{ backup, passphrase?, display_name?, replace? } — Ratspeak .rsi (ratspeak.identity.v2 + PIN) or ratspeak.identity.v1 |
{ ok, identity_hash, lxmf_hash, display_name? } (restores private key) |
| POST | /api/v1/identity/import-private |
{ private_key, display_name?, replace? } — hex / base64 / URL-safe base64 / base32 / raw key text |
{ ok, identity_hash, lxmf_hash } |
| POST | /api/v1/identity/export |
{ passphrase } — PIN ≥ 6; Ratspeak-compatible .rsi |
{ ok, backup, file_name? } |
| POST | /api/v1/identity/export-raw |
{ passphrase } — PIN ≥ 6 (same gate as .rsi); official 64-byte Reticulum identity |
{ ok, raw: { data_base64, data_hex, data_base32, file_name, identity_hash, lxmf_hash, format } } (UI save path uses data_base64 only) |
| POST | /api/v1/identity/display-name |
{ display_name } |
{ ok } |
Interfaces
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/interfaces |
{ interfaces: [], primary_local_serial_interface_id?, effective_primary_local_serial_interface_id? } |
|
| POST | /api/v1/interfaces |
{ type, name?, host?, port?, preset?, serial_port?, callsign?, mode? } |
{ ok, interface? } |
| POST | /api/v1/interfaces/primary-local-rnode |
{ id } — enabled locally connected serial interface (rnode, rnode_multi, kiss over USB, BLE, or local TCP) |
{ ok, reordered?, effective_id?, error? } |
| PUT | /api/v1/interfaces/{id} |
Partial patch (see below) | { ok, interface? } |
| DELETE | /api/v1/interfaces/{id} |
{ ok } |
|
| POST | /api/v1/interfaces/{id}/enable |
{ ok } |
|
| POST | /api/v1/interfaces/{id}/disable |
{ ok } |
|
| GET | /api/v1/rnode/presets |
{ presets: [] } |
|
| GET | /api/v1/serial/ports |
{ ports: [] } |
|
| GET | /api/v1/ble/availability |
{ available, missing, permissions_granted, probe_failed? } |
|
| GET | /api/v1/ble/scan |
timeout_secs (1–30, default 5), mode (peer | rnode | all) |
{ devices: [{ address, name?, rssi?, kind? }] } or { ok: false, error } |
PUT /api/v1/interfaces/{id} patch fields (all optional): name, type, enabled, host, port, preset, serial_port, frequency, bandwidth, txpower, spreading_factor, coding_rate, callsign, id_interval, mode (full | point_to_point | access_point | roaming | boundary | gateway; aliases ap/gw; empty clears; invalid non-empty → API error invalid interface mode: …; omitted on PUT preserves existing), discoverable, latitude, longitude, height, discovery_name, announce_interval_min, connectable, reachable_on. On POST add, omitted mode defaults to boundary (tcp/udp/i2p) or access_point (rnode/rnode_multi); Auto/BLE Peer/KISS/Pipe leave mode unset.
The Connection tab UI edits a subset: name and mode for all types; host / port for TCP/UDP; serial_port, preset, callsign for RNode. Enable/disable uses the dedicated POST routes.
Config and stack settings
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/config |
{ content } |
|
| PUT | /api/v1/config |
{ content } (full rnsd INI text) |
{ ok } |
| GET | /api/v1/config/export |
{ content } |
|
| POST | /api/v1/config/import |
{ content, mode: merge\|replace } |
{ ok, warnings? } |
| GET | /api/v1/config/audit |
{ issues: ConfigAuditIssue[] } — config vs live interface audit |
|
| POST | /api/v1/config/repair |
{ repair_kinds?: string[] } — repair_config, apply_preset, add_auto, disable_share_instance (empty = all) |
{ ok, repaired: string[], restart_required: bool } |
| GET | /api/v1/stack/settings |
{ enable_transport, share_instance, loglevel, announce_interval_sec } — announce_interval_sec defaults to 3600 (1 h) when absent from config |
|
| PUT | /api/v1/stack/settings |
Full StackSettings JSON (all four fields recommended) |
{ ok } — missing announce_interval_sec deserializes as 0 |
| POST | /api/v1/stack/restart |
{ ok } |
|
| DELETE | /api/v1/announces |
{ ok } — clears stub persisted peers; live path table may repopulate under rns-stack |
|
| POST | /api/v1/announces |
{ ok } — send LXMF delivery announce now (live stack). Interval scheduling also sends startup + periodic announces from announce_interval_sec |
Config bootstrap (stack start): When announce_interval_sec is missing from rnsd config, the sidecar writes 3600; explicit 0 is left unchanged (ensure_announce_interval_sec_default in reticulum-sidecar/src/stack/config.rs). Missing share_instance / instance_name are filled as No / mesh-client (explicit values are preserved). Same bootstrap pass may set discover_interfaces = Yes for RMAP ingest.
Path medium preference and pins
Routing bias between RF (LoRa / RNode) and network (TCP/UDP/I2P/gateway/shared-instance) path slots. Backed by rsReticulum TransportQuery::SetPathMediumPreference / SetPeerMediumPin / GetPathSlots; persisted in mesh_client_stack.json as path_medium_preference (default "lowest") and peer_medium_pins ({ "<32 hex dest>": "rf" | "network" }, max 256 entries).
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/settings/path-medium-preference |
{ ok, preference: "lowest"\|"network"\|"rf", pins: { "<hash>": "rf"\|"network" } } |
|
| PUT | /api/v1/settings/path-medium-preference |
{ preference: "lowest"\|"network"\|"rf" } |
{ ok, preference }; 400 { ok: false, error: "invalid_path_medium_preference" } on an unknown token. On success emits path_medium_preference WS ({ preference }) |
| GET | /api/v1/peers/{hash}/paths |
{ ok, destination_hash, preference, pin, effective_preference, live, paths: PathSlot[] }; 400 on a non-32-hex hash |
|
| PUT | /api/v1/peers/{hash}/medium-pin |
{ pin: "rf"\|"network"\|null } (null clears) |
{ ok, destination_hash, pin }; 400 "pin_required" (key absent), "invalid_pin", or bad hash. Emits peers_updated WS |
PathSlot fields: active (route currently used for outbound), hops, via_hash (immediate transport id, may be null), interface (live interface name), interface_id, medium (rf / network), timestamp, expires, expired. Slots are ranked active-first and capped by rsReticulum MAX_PATH_SLOTS (3).
preference vs effective_preference: preference is the persisted global setting and pin the persisted per-destination override; effective_preference is what the live transport actually applies for that destination (pin resolved against the global) and is null when the stack is not live.
Offline / persist behavior: When the stack is down, PUTs persist and are applied on the next live start (LiveBridge::spawn re-applies the preference — skipped when it is the default lowest — then every pin). While the stack is live, PUTs persist only if the live apply succeeds; a failed live apply rolls back the persisted value so disk/UI cannot drift ahead of the transport. GET …/paths returns live: false with an empty paths array when there is no live transport. GET /api/v1/peers stays active-route-only and does not embed path arrays; fetch slots per destination.
preference semantics (rsReticulum): lowest applies no medium bias and ranks purely by hops; network / rf are "prefer if possible" — when the preferred medium has no live slot the other medium becomes active without clearing the preference, so the preferred medium can reclaim the route later.
LXMF and contacts
| Method | Path | Body / notes | Response |
|---|---|---|---|
| POST | /api/v1/lxmf/send |
{ destination_hash, text, reply_to_hash?, reply_to_id?, reply_preview_text? } |
Live: stamps LXMF FIELD_REPLY_TO (0x30) / optional FIELD_REPLY_QUOTE (0x31) before sign; { ok, delivery_method?, delivery_status?, sent_via?, message? } or { ok: false, error: "no_propagation_node" }. delivery_status on this response is initial enqueue state only (queued or sending) — not delivery confirmation. Before live attach (rns-stack without bridge): fail-closed { ok: false, error: "lxmf send requires live rns-stack sidecar" }. Stub build: { ok, sent_via?, message? } |
| POST | /api/v1/lxmf/paper/create |
{ destination_hash, text, reply_to_hash?, reply_to_id?, reply_preview_text? } (Chat UI currently sends destination_hash + text only) |
Live: signed DeliveryMethod::Paper + to_paper_uri (encrypt to peer pubkey in known_identities); no network send. { ok, uri, message_hash, delivery_method: "paper", message? } or { ok: false, error: "identity_unknown" \| "paper_too_large" \| "identity_not_configured" \| "invalid_hash" \| "internal_error" }. Emits WS lxmf_message outbound with delivery_method/sent_via/received_via=paper and delivery_status: "delivered" — does not emit lxmf_outbound_status. Stub: { ok: false, error: "identity_not_configured" } |
| POST | /api/v1/lxmf/paper/ingest |
{ uri } (lxm:// base64url paper blob; URI length capped before decrypt) |
Live: from_paper_uri + local identity decrypt + ingest_lxm_uri → delivery callback / WS lxmf_message inbound (delivery_method/received_via=paper). { ok, message? } or { ok: false, error: "invalid_uri" \| "decrypt_failed" \| "identity_not_configured" \| "paper_too_large" \| "internal_error" }. Stub: { ok: false, error: "identity_not_configured" } |
| POST | /api/v1/lxmf/reaction |
{ destination_hash, target_hash, emoji } |
Live: stamps standard LXMF FIELD_REACTION (0x40) msgpack map (REACTION_TO 0x00 = 32-byte parent hash, REACTION_CONTENT 0x01 = emoji) before sign and keeps the emoji as message content; an unparsable target_hash fails open (sends content-only, no 0x40). { ok, message? }. Before live attach: fail-closed { ok: false, error: "lxmf reaction requires live rns-stack sidecar" } |
| GET | /api/v1/lxmf/recent |
?since_ts= (ms, optional), ?since_seq= (opaque ring_seq, optional), ?limit= (default 200, max 500) |
{ messages: [], ring_len } — ring buffer of recent inbound LXMF payloads for WS lag/reconnect catch-up (not durable across sidecar restart; capped at 200). Rows are chronological (oldest→newest) and each accepted row is stamped with monotonic ring_seq. Cursor: since_ts alone keeps timestamp > since_ts; with since_seq, keep rows after the complete (since_ts, since_seq) cursor (timestamp > since_ts or same-ms with ring_seq > since_seq) so same-ms twins remain recoverable without reprocessing the boundary; ring_len is current buffer occupancy |
| DELETE | /api/v1/lxmf/messages/{hash} |
{ ok } |
|
| GET | /api/v1/contacts |
{ contacts: [] } — overlays announce/peer/Nomad labels onto nameless or hash-prefix contact display_name values (does not overwrite a real name) and may persist fills |
|
| DELETE | /api/v1/contacts |
{ ok, cleared } — clears LXMF contacts after demoting them into the peer cache (keeps Peers; does not delete chat messages) |
Peers, topology, and propagation
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/peers |
?refresh=1 optional |
{ peers: [] } — live path table when rns-stack enabled; without refresh=1 may serve a short-TTL maintenance cache; refresh=1/true forces live GetPathTable (manual Refresh). display_name overlayed from contacts/Nomad/announce label cache; optional public_key (128 hex) when known from announces |
| POST | /api/v1/peers/{hash}/path |
{ ok } — emits peers_updated WS on success |
|
| POST | /api/v1/peers/{hash}/probe |
{ ok, hops? } live; { ok, mode, hash } stub — emits peers_updated on success |
|
| POST | /api/v1/ping |
{ destination_hash } |
{ ok, rtt_ms? } |
| GET | /api/v1/topology |
{ nodes, edges, total?, shown?, truncated? } — via_hash is the immediate RNS next hop (transport id); sidecar infers self → relay when needed |
|
| GET | /api/v1/rmap/discovered |
{ discovered: RmapDiscoveredWireRow[] } — local RMAP v4 heard interfaces (7-day TTL eviction in rsReticulum DiscoveryStore) |
RmapDiscoveredWireRow fields (see src/shared/reticulum-types.ts): discovery_hash, transport_id, discovery_name, interface_type, latitude, longitude, height, transport_enabled, reachable_on, LoRa RF fields (frequency, bandwidth, spreading_factor, …), hops, stamp_value, discovered, last_heard, heard_count, status (available/stale/unknown), has_coordinates. Renderer caps at 2,000 newest rows with client-side TTL eviction.
WS rmap.discovery: sidecar polls DiscoveryStore every 10s; emits full { discovered: [...] } snapshot when JSON fingerprint changes. Stub builds return { discovered: [] }.
| GET | /api/v1/packets | ?limit=500 (1–2500) | { packets: [] } — recent wire tap ring buffer |
| DELETE | /api/v1/packets | | { ok } — clear wire tap buffer |
| GET | /api/v1/propagation | | { propagation, preferred_id, auto_sync_interval_sec, propagation_mode, propagation_auto_blacklist, pn_hosting_policy, last_propagation_sync_at? } — local-prop rows include message_count, storage_bytes when live |
| GET | /api/v1/propagation/discovered | | { discovered: DiscoveredPropagationRow[] } — heard lxmf.propagation announces (not auto-configured) |
| POST | /api/v1/propagation/add | { destination_hash, name?, skip_probe? } | { ok, node } or { ok: false, error } — probes /offer unless skip_probe; may return PROPAGATION_OFFER_UNSUPPORTED, PROPAGATION_PEER_COST_EXCEEDS_MAX, identity/path errors |
| POST | /api/v1/propagation/hosting-policy | PnHostingPolicy | { ok } — persist + apply local PN hosting / peering policy |
| PUT | /api/v1/propagation/{id} | { name } | { ok } — rename a remote node (local-prop rejected) |
| DELETE | /api/v1/propagation/{id} | | { ok } — remove a remote node (local-prop rejected; clears preferred if that id) |
| POST | /api/v1/propagation/{id}/enable | | { ok } — for local-prop, starts PN serve + announce (waits for messagestore load — see below) |
| POST | /api/v1/propagation/{id}/disable | | { ok } — for local-prop, stops PN serve + announce |
| POST | /api/v1/propagation/{id}/preferred | | { ok } |
| POST | /api/v1/propagation/mode | { mode: "off"\|"auto"\|"manual" } | { ok } — gates outbound PN cascade + Auto sync eligibility |
| POST | /api/v1/propagation/auto-blacklist | { destination_hash } | { ok } — Ignore for Auto (sync + deposit); 32-hex; cap 256 |
| DELETE | /api/v1/propagation/auto-blacklist/{hash} | | { ok } — Allow Auto again |
| POST | /api/v1/propagation/sync | { propagation_id? } or { destination_hash? } | { ok } — client /get-primary for remotes; local-prop drains in-process. Errors include PROPAGATION_PATH_UNKNOWN, PROPAGATION_RETRIEVE_BUSY, PROPAGATION_SYNC_OUTBOUND_BUSY, PROPAGATION_STACK_NOT_LIVE, identity/non-PN/peering codes |
| POST | /api/v1/propagation/sync/cancel | | { ok } — cancels Sync UI run and aborts in-flight client /get (abort_transfer) |
| POST | /api/v1/propagation/auto-sync-interval | { interval_sec } | { ok } — 0 disables periodic sync; persists with stack |
Deferred PN messagestore: On live attach, the local propagation messagestore loads in the background (spawn_blocking) so a large disk store does not gate TCP/LXMF/RRC readiness. New writes still go to storage_dir. If local-prop is enabled, serve/announce starts only after that load finishes (avoids advertising an empty PN while the scan runs).
PnHostingPolicy (mirrored in src/shared/pnHostingPolicy.ts / sidecar pn_hosting_policy.rs):
| Field | Default | Notes |
|---|---|---|
peering_cost |
18 |
Must be ≤ max_peering_cost |
max_peering_cost |
26 |
|
autopeer |
true |
|
autopeer_maxdepth |
4 |
Cap 64 |
max_peers |
20 |
1–256 |
propagation_stamp_cost |
16 |
|
propagation_stamp_flex |
3 |
Must be ≤ stamp cost |
message_storage_limit_mb |
256 |
1–10240 |
propagation_limit_kb |
256 |
1–102400 |
sync_limit_kb |
10240 |
1–102400 |
delivery_limit_kb |
1000 |
1–102400 |
from_static_only |
false |
|
auth_required |
false |
|
enforce_stamps |
false |
Persisted for UI/compat; not applied on current rsLXMF tip (set_enforce_stamps removed upstream; stamp gating uses set_stamp_requirements) |
enforce_ratchets |
false |
Persisted for UI/compat; not applied on current rsLXMF tip (set_enforce_ratchets removed upstream) |
static_peers |
[] |
Lowercase 32-hex hashes (max 256) |
node_name |
null |
Trimmed; max 128 scalar chars; no control chars |
pn_announce_interval_sec |
360 |
Cap 86400 |
announce_at_start |
true |
Nomad Network
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/nomadnetwork/nodes |
{ nodes: [] } |
|
| POST | /api/v1/nomadnetwork/nodes/favorite |
{ hash, favorited } |
{ ok } |
| GET | /api/v1/nomadnetwork/page/{hash}?path=… |
optional data, force_path_refresh=true (stale-path retry) |
page payload |
| GET | /api/v1/nomadnetwork/file/{hash}?path=… |
optional force_path_refresh=true |
{ ok, file_name?, content_base64? } |
| GET | /api/v1/nomadnetwork/serving |
{ ok, serving } (local host status; includes content_source, content_layout, watcher_status, last_error) |
|
| PUT | /api/v1/nomadnetwork/serving |
{ enabled, display_name? } |
{ ok, serving } |
| PUT | /api/v1/nomadnetwork/serving/content-source |
{ path: string } |
{ ok, serving } — set watched folder (site root or pages dir); required before start; restarts host if running |
| GET | /api/v1/nomadnetwork/serving/pages |
{ ok, pages: [] } — My Pages lists these read-only; edit pages on disk in the watched folder |
|
| PUT | /api/v1/nomadnetwork/serving/pages |
{ path, content } |
{ ok } — sidecar-only; not exposed in the My Pages UI |
| DELETE | /api/v1/nomadnetwork/serving/pages?path=… |
{ ok } — sidecar-only; not exposed in the My Pages UI |
|
| GET | /api/v1/nomadnetwork/serving/page?path=… |
{ ok, path, content } |
|
| GET | /api/v1/nomadnetwork/serving/files |
{ ok, files: [] } — My Pages lists these read-only |
|
| PUT | /api/v1/nomadnetwork/serving/files |
{ path, content_base64 } |
{ ok } — sidecar-only; not exposed in the My Pages UI |
| DELETE | /api/v1/nomadnetwork/serving/files?path=… |
{ ok } — sidecar-only; not exposed in the My Pages UI |
Auto-restore order when the live stack comes up: load nomad_serving_content_source → if nomad_serving_enabled and a content source is set, start hosting → start FS watcher on pages/ (and files/ when that directory exists). If enabled without a content source, set last_error=content_source_required and do not start. Other failures keep enabled=true with running=false and last_error set; logs use the [nomad-serving] tag.
RRC (Reticulum Relay Chat)
Multi-hub sessions are keyed by lowercase 32-hex hub_dest_hash. Soft cap: 8 simultaneous hubs. Nick / room / body lengths are capped at the API boundary.
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/rrc/hubs |
{ hubs: RrcHubInfo[] } |
|
| POST | /api/v1/rrc/hubs |
{ dest_hash, label?, favorited? } |
{ ok, hub? } |
| POST | /api/v1/rrc/hubs/favorite |
{ dest_hash, favorited } — dest_hash must be exact 32 hex |
{ ok } |
| POST | /api/v1/rrc/connect |
{ dest_hash, nickname? } |
{ ok } / { ok: false, error } |
| POST | /api/v1/rrc/disconnect |
{ dest_hash? } — omit/empty tears down every hub |
{ ok } |
| GET | /api/v1/rrc/status |
{ sessions: [], identity_hash } |
|
| POST | /api/v1/rrc/join |
{ hub_dest_hash, room, key? } |
{ ok } |
| POST | /api/v1/rrc/part |
{ hub_dest_hash, room } |
{ ok } |
| POST | /api/v1/rrc/send |
{ hub_dest_hash, room?, body, type?, dst_hash? } |
{ ok } |
| POST | /api/v1/rrc/nick |
{ nickname, hub_dest_hash? } — omit hub applies to every session |
{ ok } |
| GET | /api/v1/rrc/rooms |
?hub_dest_hash= optional |
{ rooms: [] } |
Typed renderer wrappers: electronAPI.reticulum.rrc.* (listHubs, upsertHub, setFavorite, connect, disconnect, getStatus, join, part, send, setNickname, getRooms).
Remote — rnsh (shell) and rncp (file transfer)
Stock Reticulum utility clients hosted by the sidecar. Paths are filesystem paths only (never file bytes in JSON). Electron gates rncp.send / rncp.fetch / rncp.listener mutations behind picker-allowlisted IPC (electronAPI.reticulum.rncp.*).
| Method | Path | Body / notes | Response |
|---|---|---|---|
| POST | /api/v1/rnsh/connect |
{ destination_hash } |
{ ok, session_id?, identity_hash?, fingerprint?, error? } |
| POST | /api/v1/rnsh/input |
{ session_id, data, encoding? } — encoding: "base64" optional |
{ ok, error? } |
| POST | /api/v1/rnsh/resize |
{ session_id, rows?, cols? } |
{ ok, error? } |
| POST | /api/v1/rnsh/disconnect |
{ session_id } |
{ ok, error? } |
| GET | /api/v1/rnsh/status |
{ sessions: [] } |
|
| POST | /api/v1/rncp/send |
{ destination_hash, path } |
{ ok, transfer_id?, error? } |
| POST | /api/v1/rncp/fetch |
{ destination_hash, remote_path, save_path? } |
{ ok, transfer_id?, error? } |
| POST | /api/v1/rncp/cancel |
{ transfer_id } |
{ ok, error? } |
| POST | /api/v1/rncp/accept |
{ transfer_id } |
{ ok, … } / { ok: false, error } |
| POST | /api/v1/rncp/reject |
{ transfer_id } |
{ ok, error? } |
| GET | /api/v1/rncp/status |
{ transfers: [] } |
|
| POST | /api/v1/rncp/announce |
{} |
{ ok } / { ok: false, error: "listener_not_enabled" } |
| GET | /api/v1/rncp/listener |
{ enabled, destination_hash?, inbound_mode, allowed, blocked } |
|
| POST | /api/v1/rncp/listener |
{ enabled, save_dir?, allow_fetch?, fetch_jail?, overwrite?, allowed?, blocked? } |
{ ok, error? } |
| POST | /api/v1/remote/path-capability |
{ destination_hash } |
PathCapability JSON (speed, via_atoms, transfer_allowed, shell_allowed, reason_key?) |
| GET | /api/v1/remote/identity |
{ identity_hash, rncp_receive_hash? } |
WS event types (in addition to the list below): rnsh.stdout, rnsh.stderr, rnsh.status, rnsh.closed, rnsh.error, rncp.offer, rncp.progress, rncp.completed, rncp.failed, rncp.cancelled. Payloads always include session_id or transfer_id.
Caps (local policy): max 8 shells, max 3 concurrent transfers, default 25 MiB file size; LoRa/BLE-only destinations refuse transfers (path_constrained) before opening a link.
Listener persistence: a successful POST /api/v1/rncp/listener stores the config (rncp_listener_* keys in mesh_client_stack.json); the live stack restores the listener on start, so the inbound mode survives sidecar/app restarts. enabled: false persists only the flag and keeps the last dir/policy fields.
System
| Method | Path | Body / notes | Response |
|---|---|---|---|
| GET | /api/v1/diagnostics |
Reticulum-native health snapshot — includes announce_ws coalesce pressure (last_window_ingress / unique / overflow, last_storm_at_ms, last_flush_at_ms). Renderer Diagnostics emits reticulum/announce-bus-pressure when lag/storm/overflow is recent. |
|
| POST | /api/v1/system/factory-reset |
{ ok } — Electron UI must call electronAPI.reticulum.factoryReset (generic proxyPost blocks this path) |
|
| GET | /api/v1/voice/status |
LXST telephony status (available, enabled, running, microphone_muted, active_call) |
|
| POST | /api/v1/voice/call |
{ identity_hash } |
Place Opus call (QualityHigh, ~15s discovery). identity_hash is 32-hex identity (not LXMF dest). |
| POST | /api/v1/voice/answer |
Answer incoming call | |
| POST | /api/v1/voice/reject |
Reject ringing call | |
| POST | /api/v1/voice/hangup |
End active call | |
| POST | /api/v1/voice/mute |
{ muted } |
Renderer mute flag (sidecar drops PCM ingest) |
| POST | /api/v1/voice/audio |
{ profile?, channels, samples_b64 } |
Push one PCM frame (LE f32 base64) for Opus TX. Only established calls transmit; earlier frames are accepted-and-dropped as not_established (soft-drop — do not fatal). Renderer must defer capture/TX until voice.update status established (Answer only warms AudioContext). Use dedicated IPC reticulum:voiceSendAudio (own ~2000/min budget); generic reticulum:proxyPost rejects this path so realtime PCM does not starve the shared 900/min proxy ceiling. |
| POST | /api/v1/voice/memo/start |
Start LXMF voice-memo encode session (AM_OPUS_OGG / QualityMedium). Dedicated IPC reticulum:voiceMemoStart — generic proxyPost rejects /api/v1/voice/memo/*. |
|
| POST | /api/v1/voice/memo/audio |
{ session_id, channels, samples_b64 } |
Push one 24 kHz / 60 ms (1440 f32) PCM frame for memo encode. Dedicated IPC reticulum:voiceMemoSendAudio (rate-limited like live voice PCM). |
| POST | /api/v1/voice/memo/stop |
{ session_id } |
Finish encode; returns { ok, ogg_base64, duration_ms, size_bytes, mode }. Cap ~240 KiB. Dedicated IPC reticulum:voiceMemoStop. |
| POST | /api/v1/voice/memo/cancel |
{ session_id } |
Drop memo session without producing Ogg. Dedicated IPC reticulum:voiceMemoCancel. |
| GET | /api/v1/games/status |
LRGP live status (available, enabled, running, registered apps). Use dedicated IPC reticulum:gamesStatus — generic proxyGet rejects /api/v1/games/* |
|
| GET | /api/v1/games/apps |
Registered game manifests (ttt, chess) | |
| GET | /api/v1/games/sessions |
optional ?peer= |
Session list (sidecar LrgpStore) |
| GET | /api/v1/games/sessions/:id |
Session detail + board metadata; JSON may include overlay delivery_state (sending / propagating / failed / …) |
|
| POST | /api/v1/games/action |
{ dest_hash, app_id, command, session_id?, payload? } |
Send LRGP action (challenge/accept/move/…). Dedicated IPC reticulum:gamesAction (~600/min own bucket). Enqueue failure emits games.action_result { ok: false } after sidecar rollback |
| POST | /api/v1/games/sessions/:id/resend |
Resend last envelope (same nonce). Envelope bytes persist in companion games_outbound.db across sidecar restart |
|
| POST | /api/v1/games/sessions/:id/read |
Mark session read | |
| DELETE | /api/v1/games/sessions/:id |
Delete session | |
| GET | /api/v1/identities |
{ identities: […] } — slots under config/identities/<id>/ + active_identity; working key remains config/identity; flat identity migrates to identities/default/ |
|
| POST | /api/v1/identities |
{ display_name? } |
{ ok, id, identity } or { ok: false, error } — rns-stack only; stage slot → apply working key → commit pointer last (rollback on failure). Cap 16 slots. Errors: identity_slot_limit_reached, display_name_*. Emits restart. |
| POST | /api/v1/identities/switch |
{ identity_id } |
{ ok } or { ok: false, error } — stash, install target → working, reconcile, then commit pointer. Errors: identity_slot_not_configured, identity_not_found |
| POST | /api/v1/identities/delete |
{ identity_id } |
{ ok } or { ok: false, error } — refuses cannot_delete_active_identity / cannot_delete_last_identity |
WebSocket
GET /ws — server push JSON text frames:
{ "type": "lxmf_message", "payload": { ... } }
Event types: lxmf_message, lxmf_outbound_status, events_lagged (WS subscriber skipped N broadcast frames — client should GET /api/v1/lxmf/recent), announce.received, peers_updated, path_medium_preference (global preference changed; payload { preference }), stats_update, interface.state, stack_restart_requested, propagation_sync, propagation.discovered (heard lxmf.propagation announce), resource.received, rmap.discovery (payload { discovered: RmapDiscoveredWireRow[] }), nomadnetwork.node (Nomad peer announce heard), nomad.serving_start / nomad.serving_stop (local hosting lifecycle; payload includes destination_hash / display_name on start — renderer currently polls serving status via HTTP), RRC: rrc.hub, rrc.connected, rrc.disconnected, rrc.room.joined, rrc.room.parted, rrc.message, rrc.error, plus Remote: rnsh.stdout / rnsh.stderr / rnsh.status / rnsh.closed / rnsh.error, rncp.offer / rncp.progress / rncp.completed / rncp.failed / rncp.cancelled, plus LXST voice signalling: voice.update / voice.incoming / voice.terminated / voice.error / voice.stats, plus LRGP games: games.update / games.action_result (turn-based; stay on shared /ws).
Note: Live wire_packet frames are not pushed on /ws (they starved critical lxmf_message events on large meshes). Sniffer/Stats poll GET /api/v1/packets while those panels are mounted. PacketTap rows still feed the sidecar packet log and LXMF egress evidence.
GET /ws/voice: dedicated high-rate stream for LXST PCM receive frames (voice.audio with link_id, profile, channels, samples_b64). Same starvation rationale as packet tap — do not put realtime audio on shared /ws. Electron main bridges this to reticulum:voiceAudio (not reticulum:event).
-
rrc.disconnected: payload{ hub_dest_hash, reason, will_reconnect? }. Whenwill_reconnectisfalse(orreasonislocal_disconnect), the renderer drops that hub session. Whentrue(or omitted on older sidecars), the UI shows reconnecting and keeps volatile rooms until WELCOME. -
Outbound Direct backchannel: On live stack start,
LinkDeliveryManager::set_inbound_packet_sender(spawn_lxmf_outbound_backchannel(...))forwards plaintext on outbound-initiated reusable Direct links into the same unpack path as peer-initiatedlxmf.delivery. Developer log marker:LXMF outbound-link backchannel packet. Without this wiring, the peer's first reply may Ack on their client but never appear in mesh-client Chat. lxmf_outbound_status: authoritative outbound delivery updates for network sends. Payload:{ message_hash, status, delivery_method?, to_hash?, sent_via? }wherestatusisdelivered,stored_locally,failed, or intermediatesending(egress upgrade or Direct→PN cascade step). mesh-client mapsdelivered/stored_locally→ UI Completes (acked) and persistsdelivery_status(+delivery_methodwhen present) to SQLite; remote PN Completes (delivered) show Stored at propagation node; local-prop Completes (stored_locally) mean deposited on your hosted PN (full PN parity — peer/offersync may still propagate; house badge ≠ dead-end outbox);failed→ Failed. Do not treat/api/v1/lxmf/sendresponsedelivery_status(queued/sending) as terminal. After Direct exhausts, the sidecar cascades preferred remote → other enabled remotes (hop-sorted) → local-prop last, emittingsending+delivery_method: "propagated"or"stored_locally"between attempts before a finaldelivered/stored_locally/failed. Paper create/ingest does not use this event — Completes vialxmf_messagewithdelivery_method: "paper"/delivery_status: "delivered".announce.received: coalesced WS notify for LXMF identity announces / path responses (named or nameless). Sidecar applies identity-key + display-name cache updates immediately, but emits at most one WS frame per coalesce window (500ms normal / 1000ms when >256 distinct destinations are pending) so announce storms stay O(1) bus pressure on large meshes (~100k). Payload is either a single{ destination_hash, display_name?, hops, aspect?, identity_hash? }(legacy / one-row flush) or{ announces: [{ destination_hash, display_name?, hops, aspect?, identity_hash? }, ...] }(capped at 1024, named preferred; overflow dropped — slow peer poll recovers).aspectis set when announcename_hashmaps to a known app name (lxmf.delivery,lxmf.propagation,nomadnetwork.node,rrc.hub,lxst.telephony); omitted for path responses / unknown hashes (clients must not invent"unknown").identity_hashis the hex identity recovered from the validated announce when present. Each flush publishes pressure counters underGET /api/v1/diagnostics→announce_ws(ingress/unique/overflow + storm/flush timestamps) for the Diagnosticsreticulum/announce-bus-pressurewarning. Display names update the peer-label cache only — announces do not auto-create LXMF contacts. That cache is overlayed ontoGET /api/v1/peers/ topology rows and onto nameless/hash-prefix rows fromGET /api/v1/contacts(list_contactsmay persist those fills) so path-table and contact refreshes keep announce aliases.peers_updated: also emitted when the live path table gains new destination hashes (maintenance tick). Payload may include{ added: string[], patches: PeerRow[], count }(added/patches capped at 1024). Renderer applies patches incrementally, including route-field changes. A full peer dump is used on connect, manual Refresh, restart, safety poll, or apeers_updatedpayload that cannot be applied incrementally:cleared,demoted_from_contacts, or a single-hashprobe/path event. Hop/timestamp-only churn does not emit.
lxmf_message payload fields include sender_hash, text, timestamp, message_hash, optional direction (inbound / outbound), optional delivery_status (sending on optimistic outbound rows; delivered on paper Completes), optional reply_to_hash / reply_preview_text (from LXMF FIELD_REPLY_TO / FIELD_REPLY_QUOTE), optional reaction_target (from LXMF FIELD_REACTION 0x40, present as lowercase 64-hex when decoded — a reaction takes precedence over a reply, so reply_to_hash is omitted when reaction_target is set, and the field emoji fills text only when the message content is empty), and transport markers received_via / sent_via. When FIELD_REACTION is absent or malformed the payload is unchanged (no reaction_target; reply/plain paths intact). Outbound sent_via is path-table / PacketTap evidence, not “any local RNode enabled”: atomic values are rf, ble, tcp, network, or paper (offline QR handoff); multi-egress observes join with + (e.g. rf+tcp, ble+network). Inbound received_via uses the path-table interface name matched to local interface config (same atoms — so a TCP hub named “RNS Testnet” is tcp, not network) or paper for decrypted paper URIs. Never use Meshtastic-style both for Reticulum network egress (legacy both may still appear in SQLite allowlists).
lxmf_outbound_status payload: message_hash, status (delivered / stored_locally / failed / sending), optional delivery_method, optional sent_via (egress evidence upgrade before Completes).
Electron bridge
Renderer calls electronAPI.reticulum.*; main process proxies to this API (sandboxed renderer cannot reach localhost directly). Lifecycle / proxy / Remote / factory-reset handlers live in src/main/ipc/reticulum-handlers.ts. Reticulum destination / Remote address / inbound-policy DB handlers are in src/main/ipc/reticulum-db-handlers.ts; RRC room history uses src/main/ipc/rrc-db-handlers.ts.
Shared reticulum:proxy* IPC is capped at 900/min. GET /api/v1/lxmf/recent uses a dedicated 120/min bucket so WS-lag catch-up does not starve mesh control. On rate-limit errors the renderer applies exponential backoff (reticulumProxyRateLimitBackoff.ts).
| IPC channel | Role |
|---|---|
reticulum:start / stop / getStatus |
Sidecar lifecycle |
reticulum:syncInterfaceIssueScope |
Drop TCP/TX latch entries for disabled/removed interfaces; sticky enabled-name filter for later log lines |
reticulum:proxyGet / proxyPost / proxyPut / proxyDelete |
HTTP proxy to paths above (shared 900/min; lxmf/recent 120/min) |
reticulum:factoryReset |
Factory reset (generic proxyPost blocks /api/v1/system/factory-reset; UI must use this channel) |
reticulum:validateConfig |
One-shot validate-config --json against userData/reticulum/config (read-only; safe while stack runs) |
reticulum:readDefaultConfigFile |
Read first existing system rnsd config path |
reticulum:showConfigImportDialog |
Native file picker for config import |
reticulum:showIdentityImportDialog |
Native file picker for 64-byte private key (.retid, .key, …) |
reticulum:showNomadContentSourceDialog |
Native folder picker for Nomad My Pages content source (site root or pages/ dir); records picker allowlist |
reticulum:setNomadContentSource |
Apply Nomad watched content source; path must match last folder-picker result (blocks arbitrary proxyPut) |
reticulum:rncpSend / rncpFetch / setRncpListener |
Picker-gated rncp send/fetch/listener (path must match reticulum-remote-paths allowlist) |
reticulum:showRncpOpenFileDialog / showRncpSaveDirectoryDialog |
Native pickers that seed the rncp send-file / save-dir+fetch-jail allowlists |
reticulum:revealInFolder |
Reveal a path in the OS file manager when it matches an rncp picker allowlist |
reticulum:event / reticulum:status |
Shared /ws events and sidecar status (preload: onEvent / onStatus) |
reticulum:voiceSendAudio |
Dedicated PCM TX ingest (POST /api/v1/voice/audio); own ~2000/min budget (not generic proxyPost) |
reticulum:voiceAudio |
Dedicated /ws/voice PCM frames (voice.audio; preload: onVoiceAudio) |
electronAPI.reticulum.voice.* |
Preload surface: getStatus / call / answer / reject / hangup / mute / sendAudio |
reticulum:voiceMemoStart / voiceMemoSendAudio / voiceMemoStop / voiceMemoCancel |
Dedicated LXMF voice-memo encode IPC; generic proxy rejects /api/v1/voice/memo/* |
electronAPI.reticulum.voiceMemo.* |
Preload surface: start / sendAudio / stop / cancel |
reticulum:gamesStatus / gamesApps / gamesSessions / … |
Dedicated LRGP games IPC (~600/min); generic proxy rejects /api/v1/games/* |
electronAPI.reticulum.games.* |
Preload: getStatus / listApps / listSessions / getSession / sendAction / resend / markRead / deleteSession |
getStatus / onStatus may include interfaceIssueAlert (TCP connect failures, TX queue drops — including BLE / bond-stale cause keys used by Diagnostics and Connection hints, link-delivery timeouts, transport saturation / slow queries, bleBondRemoved stale RNode bonds (sticky until stack stop / interface remove; not pruned solely by the 5‑minute window), blePairingTimedOut OS passkey / TX-read timeouts). Per-entry latch timestamps use a 5-minute stale window (RETICULUM_INTERFACE_ISSUE_ALERT_STALE_MS) for most issues; Connection syncs enabled interface names via syncInterfaceIssueScope so disabling or removing an interface clears that name immediately and rejects re-latch from lagging log lines. Stopping the stack (or unexpected process exit) clears the tracker.
propagation_sync WebSocket payload: { active: boolean, progress: number, message: string | null }. Progress uses 0–100 driven by the client /get download (Establishing ≈10 … Complete ≈100). Sticky success emits active:false, progress:100; cancel/stall/failure emit active:false, progress:0 (and must not emit a trailing 100). Sync POST /api/v1/propagation/sync may return PROPAGATION_PATH_UNKNOWN, PROPAGATION_RETRIEVE_BUSY, PROPAGATION_SYNC_OUTBOUND_BUSY, PROPAGATION_STACK_NOT_LIVE, PROPAGATION_IDENTITY_UNKNOWN, PROPAGATION_TARGET_NOT_PN, PROPAGATION_PEERING_STAMP_FAILED, PROPAGATION_PEER_COST_EXCEEDS_MAX, or LOCAL_PROPAGATION_SYNC_UNSUPPORTED. Add may return PROPAGATION_OFFER_UNSUPPORTED / probe timeout failures.
SQLite chat history uses separate db:* handlers (getReticulumMessages, saveReticulumMessage, searchReticulumMessages, deleteReticulumMessage, destination upserts), not sidecar HTTP. Remote saved addresses / inbound policy and RRC room history also use dedicated db:* handlers (not sidecar HTTP).
| IPC channel | Reticulum maintenance behavior |
|---|---|
db:pruneReticulumDestinationsByCount |
Prunes excess non-favorited destinations by oldest last_heard; favorites are preserved. |
db:deleteReticulumDestinationsByAge |
Deletes non-favorited destinations before a calculated Unix-seconds last_heard cutoff. |
db:pruneReticulumIdentityActivityByAge |
Deletes identity-activity rows before an epoch-milliseconds last_seen cutoff. |
db:upsertReticulumIdentityActivityBatch |
Validates and upserts at most 500 activity rows per call. |
db:listReticulumRemoteAddresses / upsert / delete |
Saved Remote (rnsh/rncp) addresses. |
db:listReticulumInboundPolicy / upsert / delete |
Per-identity inbound allow/block policy for rncp. |
db:listRrcMessages / insertRrcMessage / deleteRrcMessagesByRoom |
Persist / clear RRC room history (rrc_messages). |
db:pruneRrcMessagesByCount / db:pruneRrcMessagesByAge |
RRC retention (default enabled, 10,000 count / 30-day age). |
Reticulum startup maintenance runs the destination age/count prune, Reticulum message retention, and RRC message retention independently. VACUUM runs only after the Reticulum startup prune, never on the six-hour session tick.