The default server binds to 127.0.0.1:8000. These APIs control its physical
station devices in normal mode. Use the silent demo server for
experiments. Do not expose this control API to the public internet.
GET / returns the page with a meta[name="station-token"] value. Every
POST/PUT/PATCH/DELETE requires that value in X-Station-Token. JSON calls use
Content-Type: application/json; file uploads use multipart form data. Origin,
trusted-host and cross-site checks also apply. Reload/retrieve the page after
a server restart, because the token changes. It is not an account password or
a token to include in URLs, screenshots, Git commits or logs.
Example using the Python standard library against a silent demo on port 8001:
import json, re, urllib.request
base = 'http://127.0.0.1:8001'
page = urllib.request.urlopen(base + '/').read().decode()
token = re.search(r'name="station-token" content="([^"]+)"', page).group(1)
body = json.dumps({'sender': 'LAB-1', 'text': 'Research example'}).encode()
request = urllib.request.Request(base + '/api/transfers', data=body,
headers={'Content-Type': 'application/json', 'X-Station-Token': token})
transfer = json.load(urllib.request.urlopen(request))
print(transfer['id']) # Saved to outbox; this call does not play audio.Errors are JSON { "error": "…" } with an HTTP status. Typical cases: 400 invalid
input, 403 missing/expired token or cross-origin request, 409 busy/conflict,
413 oversized input, 503 unavailable native service. Avoid retrying TX blindly.
| Method / route | Body or result |
|---|---|
GET /api/status |
General station information |
GET /api/messages |
{messages: [...]} |
DELETE /api/messages |
Clear displayed message history, retain transfers |
DELETE /api/messages/<id> |
Delete one message |
GET /api/transfers |
{items: [...]} with integrity, progress and missing indices |
POST /api/transfers |
JSON {sender, text} or multipart sender + file; prepares, does not transmit |
GET /api/transfers/<id>/packets |
{packets: [...]}; optional ?parts=0,2,3 |
GET /api/transfers/<id>/download |
Original outgoing or verified incoming payload |
DELETE /api/transfers/<id> |
Delete transfer, blocked during its active TX |
POST /api/transfers/<id>/sent |
Record local playback; does not acknowledge remote delivery |
POST /api/rx |
One decoded packet object; validates and ingests it |
Raw packet fields are id, kind, name, sender, index, total, size,
sha256, data (base64). Indices are zero-based in the API and one-based
in operator resend lists. See PROTOCOL.md for exact bounds.
Do not pass the UI's one-based numbers directly to packet selection.
| Method / route | Body or result |
|---|---|
GET /api/modem/status |
Worker availability/PID, devices/settings, meters, preview, job, calibration and events |
GET /api/modem/devices |
{devices: [{deviceId, label, kind}, ...]} on the server's computer |
POST /api/modem/settings |
Partial input/output device, threshold, auto_rx, keep_awake settings |
POST /api/modem/rx/start |
Optional {device}; opens RX and saves enabled intent |
POST /api/modem/rx/stop |
Stops RX and clears enabled intent |
POST /api/modem/tx |
{transfer_id, parts?: [0, ...], settings?: {...}}; starts audible TX |
POST /api/modem/tx/stop |
Stops ordinary TX or calibration |
POST /api/modem/calibration/start |
`{mode: "local" |
POST /api/modem/calibration/stop |
Stops calibration and restores prior state |
POST /api/modem/export-wav |
Transfer/parts/settings like TX; returns WAV without playing it |
POST /api/modem/test-wav |
Optional {level}; fresh 100-baud microphone test WAV |
POST /api/modem/decode-wav |
Multipart file; imports valid NM packets from a WAV |
TX settings: baud 100/300, repeats 1–3, level 0.05–0.9, lead 0.1–3
seconds, tail 0–2 seconds, gap 500–5000 milliseconds. The UI TX
slider displays percentages, so 18% is 0.18. Calibration caps max_level
to 0.1–0.65; the UI offers 18%, 38%, 60%. RX threshold is -65 to -15 dBFS.
The calibration profile and rx_enabled cannot be injected with the ordinary
settings endpoint; they are managed by verified calibration and RX controls.
Only one TX/calibration runs at a time. Settings mutations and ordinary RX/TX controls are blocked while calibrating; Stop remains available. Calibration traffic is reserved and does not create chat/files. Status is polled by the UI about every 400 ms; long polling or a websocket is not required.
POST /api/legacy/encode accepts {mode: "1"|"2", text} and returns a job/folder.
POST /api/legacy/upload accepts multipart files (1–32 WAV parts).
GET /api/library lists legacy folders; GET /api/job/<id> polls work.
POST /api/decode/<folder_id> decodes a saved folder;
POST /api/delete/<folder_id> deletes it; GET /files/<folder_id>/<filename>
retrieves a job artifact. Folder IDs are server-issued, not arbitrary paths.
The parent talks to nm_minimodem --service using bounded JSON lines containing
an incrementing numeric id and op. Replies echo the ID with ok, optional
data or error; asynchronous events include level, packet, tx_done and
probe_done. See native/station.c and webapp/native_modem.py for the actual
implementation. Do not bypass the supervisor while another station owns audio.
Native WAV CLI gap uses seconds, unlike the HTTP/GUI millisecond setting:
{"packets": ["<CRC-valid raw packet hex>"], "baud": 100, "lead": 0.6, "tail": 0.2, "level": 0.18, "gap": 1}Pipe that JSON into ./nm_minimodem --encode-wav output.wav.
./nm_minimodem --decode-wav input.wav prints CRC-valid packets as JSON lines.
These file operations do not open microphones or speakers.