This walkthrough takes you from an empty Reticulum installation to
exchanging traffic over an RNS Link — all via the rnsapid REST + WS API.
We assume:
- Python 3.10+ is available.
- You already have
RNSinstalled (pip install rns). - You are on a mesh — that is, you have at least one working RNS
interface configured in
~/.config/reticulum/config. If you don't yet, the Reticulum installation guide walks you through it.
Every command in this guide uses plain curl and (for the WebSocket
sections) wscat. Install wscat with npm install -g wscat if you
don't have it.
git clone <this repo>
cd ReticulumAPI
python3 -m venv .venv
.venv/bin/pip install -e .[test]
.venv/bin/rnsapid --initrnsapid --init writes ~/.config/rnsapi/config with sensible defaults
and creates identities/, certs/, logs/ under ~/.config/rnsapi/.
.venv/bin/rnsapidYou'll see:
rnsapid 0.1.0 starting
starting RNS.Reticulum (configdir=<default>, log_level=3)
RNS.Reticulum started
self-signed cert SHA-256 fingerprint: <hex>
listening on https://127.0.0.1:8000
Leave that terminal running.
In another terminal:
curl -k https://127.0.0.1:8000/health
# {"status": "ok"}
curl -k https://127.0.0.1:8000/version
# {"name": "rnsapid", "version": "0.1.0", "protocol": {"rest": 1, "ws": 1}}The -k flag tells curl to skip cert verification — the default cert is
self-signed. To view the fingerprint (so you can pin it in a browser or
verify it out-of-band):
.venv/bin/rnsapid --print-cert-fingerprintcurl -k -X POST https://127.0.0.1:8000/identities
# {"hash": "...", "public_key": "...", "path": "/Users/.../identities/....rid"}The hash field is your new identity's 16-byte truncated hash (as hex).
Save it in a shell variable:
IDENT=$(curl -sk -X POST https://127.0.0.1:8000/identities \
| python3 -c "import json,sys; print(json.load(sys.stdin)['hash'])")
echo "$IDENT"curl -k -X PUT -H 'Content-Type: application/json' \
-d "{\"hash\":\"$IDENT\"}" \
https://127.0.0.1:8000/session/active-identity
# {"active": true, "hash": "...", "public_key": "...", ...}Every subsequent destination and link you register belongs to this
identity until you clear it (DELETE /session/active-identity) or your
session ends.
curl -k -X POST -H 'Content-Type: application/json' \
-d '{"direction":"in","type":"single","app_name":"myapp","aspects":["hello"]}' \
https://127.0.0.1:8000/destinations
# {"hash": "...", "identity_hash": "...", "direction": "in", "type": "single",
# "app_name": "myapp", "aspects": ["hello"]}DEST=$(curl -sk https://127.0.0.1:8000/destinations \
| python3 -c "import json,sys; print(json.load(sys.stdin)['destinations'][0]['hash'])")
curl -k -X POST -H 'Content-Type: application/json' \
-d "{\"destination_hash\":\"$DEST\",\"app_data_b64\":\"$(printf 'hi from rnsapid' | base64)\"}" \
https://127.0.0.1:8000/announce
# {"ok": true, "destination_hash": "...", "app_data_bytes": 15}Any RNS peer on your mesh listening for announces on this app_name will see it arrive.
Open a WS listener in another terminal:
wscat -n -c wss://127.0.0.1:8000/wsYou'll receive:
{"type":"auth.session.attached","session_id":"...","is_anonymous":true}
{"type":"auth.session.connected","session_id":"...","connection_id":"..."}
Now re-run the POST /announce in the first terminal — the WS terminal
will print an announce.sent event:
{
"type": "announce.sent",
"destination_hash": "...",
"identity_hash": "...",
"session_id": "...",
"app_data_b64": "aGkgZnJvbSBybnNhcGlk"
}If any peer on your mesh announces itself while wscat is open, you'll
also see announce.received events fanned out globally.
Once you've seen announces from other peers, RNS knows how to route to them:
curl -k https://127.0.0.1:8000/paths
# {"paths": [{"hash":"...","via":"...","hops":3,"interface":"..."}, ...]}Filter by destination or interface:
curl -k "https://127.0.0.1:8000/paths?destination=abc123..."
curl -k "https://127.0.0.1:8000/paths?interface=AutoInterface[default]"If you know a destination hash but don't have a path yet, block until one arrives (or time out):
curl -k -X POST -H 'Content-Type: application/json' \
-d '{"destination_hash":"abc123...","timeout":30}' \
https://127.0.0.1:8000/paths/request
# 200: {"found": true, "destination_hash":"...", "hops":..., "next_hop":"...", "interface":"..."}
# 408: {"found": false, "destination_hash":"..."}While the request is outstanding, your session's WS receives a
path.request.sent event.
Given a known target identity + app_name + aspects, encrypt and dispatch a Packet:
curl -k -X POST -H 'Content-Type: application/json' \
-d "{
\"identity_hash\":\"$IDENT\",
\"app_name\":\"myapp\",
\"aspects\":[\"messaging\"],
\"data_b64\":\"$(printf 'hello over packet' | base64)\"
}" \
https://127.0.0.1:8000/packets/sendYour WS receives packet.sent. If the target replies with a delivery
proof, you'll also see packet.receipt.delivered.
On a destination you own:
curl -k -X POST -H 'Content-Type: application/json' \
-d "{\"destination_hash\":\"$DEST\"}" \
https://127.0.0.1:8000/packets/listen
# {"ok": true, "destination_hash": "..."}Every packet delivered to that destination now fires a packet.received
event on your session's WS connections.
Links are long-lived encrypted channels with forward secrecy.
curl -k -X POST -H 'Content-Type: application/json' \
-d "{
\"identity_hash\":\"$IDENT\",
\"app_name\":\"myapp\",
\"aspects\":[\"messaging\"],
\"await_established\":true,
\"establishment_timeout\":15
}" \
https://127.0.0.1:8000/links
# 201: {"reused":false, "awaited":true, "link_id":"...", "status":"ACTIVE", ...}If you set "await_established": false, the response returns immediately
with the link in PENDING state. Watch the WS for link.established.
LID=<link_id from step 13>
curl -k -X POST -H 'Content-Type: application/json' \
-d "{\"data_b64\":\"$(printf 'ping' | base64)\"}" \
https://127.0.0.1:8000/links/$LID/dataEvery data frame in and out fires link.data.sent or link.data.received
on the WS.
If the remote side has registered a request handler (via
destination.register_request_handler), you can send an awaited request:
curl -k -X POST -H 'Content-Type: application/json' \
-d '{"path":"/echo","data_b64":"aGVsbG8=","timeout":10}' \
https://127.0.0.1:8000/links/$LID/request
# 200: {"ok": true, "kind": "response", "response_b64": "...", "size": ...}
# 408: request timed out
# 502: remote reported failurecurl -k -X DELETE https://127.0.0.1:8000/links/$LIDYou'll see link.closed on the WS.
For production or multi-tenant deployments, turn on bearer-token auth:
.venv/bin/rnsapid --hash-password
# password: ***
# confirm: ***
# scrypt$16384$8$1$...$...Copy that hash into ~/.config/rnsapi/config:
[auth]
enabled = true
username = admin
password_hash = scrypt$16384$8$1$...$...Restart rnsapid, then login:
curl -k -X POST -H 'Content-Type: application/json' \
-d '{"username":"admin","password":"the-password"}' \
https://127.0.0.1:8000/auth/login
# {"token":"...", "session_id":"...", "auth_required":true, "is_anonymous":false}Every subsequent REST call must include Authorization: Bearer <token>.
WS clients send {"type":"auth","token":"..."} as their first frame
within 5 seconds of connecting.
- API Reference — the full endpoint and event catalog.
- Configuration — every INI key.
- CLAUDE.md — architecture notes for contributors.