REST API
atlasd web -i <iface> serves the dashboard and a JSON API on 0.0.0.0:9800. The API is the same state every other tool reads — one snapshot, many doors — plus the mutating endpoints that make configuration changes safe to perform remotely. Every endpoint takes an optional ?interface= parameter on multi-daemon hosts, and unknown /api paths return JSON 404 {"error":"not_found"}, never a page.
The auth model in one minute
- No accounts configured: reads are open, every mutating endpoint answers
403 auth_not_configured. Safe by default on a bench; create accounts before exposing the port. - Accounts: username/password (argon2id) with optional WebAuthn passkeys — enrolling a passkey makes second-factor mandatory for that account. Sessions are signed HttpOnly cookies; login is rate-limited.
- Protection posture:
[web] protect = "writes"(default) gates mutations only;protect = "all"gates reads too — the dashboard then shows a login screen and makes zero data requests before auth. - CSRF: every mutating request carries the header
X-Atlas-CSRF: 1(and a matchingOriginwhen a browser sends one). - TLS:
[web] tls = trueserves a locally generated certificate — wanted in practice for passkeys.
Machine credentials
Two non-interactive credentials, deliberately different in power:
| Node token | API token | |
|---|---|---|
| Header | X-Atlas-Node-Token: … | Authorization: Bearer atlas_… |
| Minted by | atlasd config node-token — derived from the mesh secret, time-windowed, stateless | atlasd web token add <name> --scope read|write [--expires 30d] — secret printed once |
| Scope | Read-only, whole paths only: /api/stats, /api/config, /api/health, metrics, crash listing. Never a write, never /api/auth. | read = all GETs; write adds exactly the config-apply lifecycle, PUT /api/qos, PUT /api/position and enrolment. /api/auth/* is unreachable for any token. |
| Meant for | Fleet members reading each other under protect = "all"; scrape credentials. | CI, dashboards, automation acting as an operator. |
Fan-out endpoints (/api/fleet/*, support bundles) are deliberately excluded from node-token scope — a low-power credential must not be able to amplify itself across a fleet.
Read endpoints
The full per-interface snapshot: links (RTT, jitter, loss, capacity, quality, counters, quota), peers and their per-link views, mesh membership, routes, topology with node roles and positions, crypto posture, bonding-tax ledger.
curl -s http://node-a:9800/api/stats
{
"interface": "nexus0",
"address": "10.0.100.1/24",
"uptime_secs": 5312.4,
"strategy": "lowest_delivery_time",
"probe_interval_ms": 250,
"dead_probe_count": 5,
"identity": {
"node_name": "node-a",
"node_role": null,
"listen_port": 51820,
"mtu": 1420,
"public_key_short": "8b4c..901a"
},
"links": [
{
"id": 0,
"name": "wired",
"bind_device": "eth0",
"alive": true,
"rtt_ms": 0.42,
"jitter_ms": 0.05,
"loss_pct": 0.0,
"capacity_mbps": 100.0,
"quality_score": 0.98,
"tx_bytes": 104857,
"rx_bytes": 99231,
"quota": null
}
],
"peers": [
{
"id": 0,
"public_key_short": "6324..7876",
"tunnel_ips": ["10.0.100.2"],
"session_established": true,
"last_handshake_unix": 1786297090.9,
"links": [ { "link_name": "wired", "alive": true, "rtt_ms": 0.4 } ]
}
],
"mesh": { "enabled": false, "member_count": 0 },
"routes": [],
"total_tx_bytes": 1048576,
"total_rx_bytes": 992310
}
Array of daemon instances on this host.
curl -s http://node-a:9800/api/interfaces
["nexus0"]
Liveness verdict — 200 healthy / 503 not — with named checks: daemon alive, data path genuinely moving, links probed on cadence, peers established. Built for external monitors: curl -fsS …/api/health is a complete probe.
curl -fsS http://node-a:9800/api/health
{
"ok": true,
"interface": "nexus0",
"version": "0.2.0",
"process": { "running": true, "pid": 1234, "uptime_secs": 5312.4 },
"data_path": { "ok": true, "tun_present": true, "engine_advancing": true },
"links": { "total": 2, "alive": 2, "probed_recently": 2 },
"peers": { "total": 1, "established": 1 },
"watchdog": { "systemd": true, "interval_ms": 30000, "tripped": false },
"checks": [
{ "name": "daemon", "ok": true, "required": true,
"detail": "stats snapshot 0.4s old" },
{ "name": "data_path", "ok": true, "required": true,
"detail": "TUN present, engine advancing, links probed, traffic flowing" },
{ "name": "links", "ok": true, "required": true,
"detail": "2/2 alive, 2 probed within cadence" },
{ "name": "peers", "ok": true, "required": false,
"detail": "1/1 sessions established" }
]
}
The persistent event journal, newest first, cursor-paged. Filters: since, until, category, severity, code, limit.
curl -s "http://node-a:9800/api/journal?category=link&severity=warn&since=1h"
{
"interface": "nexus0",
"schema_version": 1,
"entries": [
{
"v": 1,
"seq": 891,
"ts": 1786297001,
"category": "link",
"severity": "warn",
"code": "link.down",
"msg": "link wired on nexus0 went down",
"fields": { "reason": "probe_timeout", "missed_probes": 5 }
}
],
"count": 1,
"next_cursor": "890"
}
Ring buffer of recent scheduler decisions — which link won each placement and at what cost. The fastest way to understand why traffic moved.
The NAT classification report from the traversal netcheck.
The live configuration with secrets replaced by __ATLAS_REDACTED__; and the JSON Schema the daemon generates from its own structs — the same document behind the configuration reference.
curl -s http://node-a:9800/api/config/schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Nexus Atlas config",
"type": "object",
"required": ["interface", "link"],
"properties": {
"interface": { "$ref": "#/definitions/InterfaceConfig" },
"link": { "type": "array", "items": { "$ref": "#/definitions/LinkConfig" } },
"peer": { "type": "array", "items": { "$ref": "#/definitions/PeerConfig" } },
"scheduler": { "allOf": [ { "$ref": "#/definitions/SchedulerConfig" } ] }
},
"definitions": { "InterfaceConfig": { }, "LinkConfig": { } }
}
Prometheus/OpenMetrics exposition for every interface on the host. On by default; follows the node's protection posture like any read.
curl -s http://node-a:9800/metrics
# HELP atlas_link_up Link liveness (1 = alive)
# TYPE atlas_link_up gauge
atlas_link_up{interface="nexus0",link="wired"} 1
atlas_link_up{interface="nexus0",link="wireless"} 1
atlas_link_rtt_seconds{interface="nexus0",link="wired"} 0.00042
atlas_link_tx_bytes_total{interface="nexus0",link="wired"} 104857
atlas_peer_last_handshake_seconds{interface="nexus0",peer="6324..7876"} 1.7862970909e+09
QoS policy document; node position; locally spooled crash reports; shell mode probe; and the anonymous auth-posture probe a client uses to decide whether to show a login.
Write endpoints
Runs the daemon's exact load pipeline over posted TOML — the same findings atlasd config check prints, without touching the running state.
curl -X POST -b "$SESSION" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"toml": "[interface]\nname = \"nexus0\"\n..."}' \
http://node-a:9800/api/config/validate
{
"ok": false,
"findings": [
{
"fatal": true,
"path": "peer[0].allowed_ips[0]",
"message": "invalid CIDR: \"10.0.100.2/33\""
},
{
"fatal": false,
"path": "scheduler.priority_order",
"message": "unknown link name \"wierd\" is ignored"
}
]
}
The commit-confirmed apply lifecycle: stage a config, commit it, and confirm within the window (default 90 s, configurable 5 s–24 h) — or the node reverts itself. The question "can I brick a remote node with a config push?" is answered by this machinery: no, it un-bricks itself. GET /api/config/apply/status reports the state machine.
curl -X POST -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"toml": "[interface]\n...", "confirm_secs": 90}' \
http://node-a:9800/api/config/stage
{
"stage_id": "9f2c41d3a8b0",
"diff": [ "…unified diff against the live config…" ],
"hot": false,
"restart": true
}
curl -X POST -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"stage_id": "9f2c41d3a8b0"}' \
http://node-a:9800/api/config/commit
{
"state": "awaiting_confirm",
"stage_id": "9f2c41d3a8b0",
"deadline": 1786297180,
"auto_confirm": "manual"
}
curl -X POST -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
http://node-a:9800/api/config/confirm
{ "state": "committed", "stage_id": "9f2c41d3a8b0" }
{ "state": "reverted", "reason": "timeout" }
Merge-update the live QoS policy (applied within the reload cadence, no restart); operator-inject a position for nodes without a feed.
curl -X PUT -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"classes": [{
"class": "Video",
"reliability": "DeadlineBounded",
"multipath": { "kind": "Fec", "overhead_pct": 25 },
"priority": 2,
"latency_budget_ms": 200
}]}' \
http://node-a:9800/api/qos
{
"classes": [
{
"class": "Video",
"reliability": "DeadlineBounded",
"multipath": { "kind": "Fec", "overhead_pct": 25 },
"priority": 2,
"latency_budget_ms": 200
},
{ "class": "Control", "...": "..." }
]
}
curl -X PUT -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"lat_deg": 42.6977, "lon_deg": 23.3219, "alt_m": 590}' \
http://node-a:9800/api/position
{
"injected": { "lat_deg": 42.6977, "lon_deg": 23.3219, "alt_m": 590.0 },
"advertised": { "lat_deg": 42.6977, "lon_deg": 23.3219, "alt_m": 590.0 }
}
Fleet configuration: sign the policy sections as the next epoch and distribute it mesh-wide, directly or as a staged canary rollout (start at a canary set, then release fleet-wide or abort).
curl -X POST -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"confirm_secs": 90, "target_roles": ["relay"]}' \
http://node-a:9800/api/config/push
{
"epoch": 7,
"hash8": "3d92c1aa",
"targeted": { "roles": ["relay"] }
}
curl -X POST -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"canary_nodes": ["8b4c901a"], "confirm_secs": 120}' \
http://node-a:9800/api/config/rollout/start
{
"rollout_id": "r-8f31",
"phase": 1,
"canary_epoch": 8,
"hash8": "5e07b2c4",
"canary_nodes": ["8b4c901a"],
"unknown_nodes": []
}
curl -X POST -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
http://node-a:9800/api/config/rollout/release
# 400 "empty_canary_set" — a rollout targeting everyone is just a push
# 409 "rollout_in_flight" — release or abort the current one first
Fleet reads
GET /api/fleet/configs and GET /api/fleet/health fan out over mesh members through the tunnel and return per-node rows — including honest reachable: false entries for nodes that did not answer. Session-only by design.
Realistic examples
# External monitor: healthy exit or non-zero
curl -fsS http://node-a:9800/api/health
# Read a locked-down (protect = "all") fleet node over the tunnel
curl -H "X-Atlas-Node-Token: $(atlasd config node-token)" \
http://10.0.100.2:9800/api/config
# CI: mint once, read forever (well, 30 days)
TOKEN=$(atlasd web token add ci --scope read --expires 30d)
curl -H "Authorization: Bearer $TOKEN" \
"http://node-a:9800/api/stats?interface=nexus0"
# Change QoS policy from automation (write scope + CSRF header)
curl -X PUT -H "Authorization: Bearer $TOKEN_W" -H "X-Atlas-CSRF: 1" \
-H "Content-Type: application/json" \
-d '{"classes":[{"class":"Video","reliability":"DeadlineBounded",
"multipath":{"kind":"Fec","overhead_pct":25},"priority":2}]}' \
http://node-a:9800/api/qos
/opt/atlas/web), served by the same process. Without them the port is API-only — which is exactly what a headless integration wants.Building a client?
Official SDKs are planned, with a published OpenAPI document shipping first — the pipeline, language lineup and scope are laid out on the SDKs page. Until then, every response shape on this page is verified against the daemon source, and the config schema is already machine-readable today.