REST API

Examples verified against the daemon sourceView as Markdown

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 matching Origin when a browser sends one).
  • TLS: [web] tls = true serves a locally generated certificate — wanted in practice for passkeys.

Machine credentials

Two non-interactive credentials, deliberately different in power:

Node tokenAPI token
HeaderX-Atlas-Node-Token: …Authorization: Bearer atlas_…
Minted byatlasd config node-token — derived from the mesh secret, time-windowed, statelessatlasd web token add <name> --scope read|write [--expires 30d] — secret printed once
ScopeRead-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 forFleet 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

GET/api/stats

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.

Request
curl -s http://node-a:9800/api/stats
Response — abbreviated
{
  "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
}
GET/api/interfaces

Array of daemon instances on this host.

Request
curl -s http://node-a:9800/api/interfaces
Response
["nexus0"]
GET/api/health

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.

Request
curl -fsS http://node-a:9800/api/health
Response — 200 when healthy, 503 otherwise
{
  "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" }
  ]
}
GET/api/journal

The persistent event journal, newest first, cursor-paged. Filters: since, until, category, severity, code, limit.

Request
curl -s "http://node-a:9800/api/journal?category=link&severity=warn&since=1h"
Response
{
  "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"
}
GET/api/scheduler/log

Ring buffer of recent scheduler decisions — which link won each placement and at what cost. The fastest way to understand why traffic moved.

GET/api/netcheck

The NAT classification report from the traversal netcheck.

GET/api/config · /api/config/schema

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.

Request
curl -s http://node-a:9800/api/config/schema
Response — abbreviated
{
  "$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": { } }
}
GET/metrics

Prometheus/OpenMetrics exposition for every interface on the host. On by default; follows the node's protection posture like any read.

Request
curl -s http://node-a:9800/metrics
Response — excerpt
# 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
GET/api/qos · /api/position · /api/crash · /api/mode · /api/auth/status

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

POST/api/config/validate

Runs the daemon's exact load pipeline over posted TOML — the same findings atlasd config check prints, without touching the running state.

Request — session-gated (not reachable via API tokens)
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
Response — ok is false only on fatal findings
{
  "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"
    }
  ]
}
POST/api/config/stage → commit → confirm | revert

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.

1 — Stage the new config
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
}
2 — Commit; the daemon re-validates, applies and restarts in place
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"
}
3 — Confirm inside the window (or the node reverts itself)
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" }
A missed window instead reports
{ "state": "reverted", "reason": "timeout" }
PUT/api/qos · /api/position

Merge-update the live QoS policy (applied within the reload cadence, no restart); operator-inject a position for nodes without a feed.

Request — thicken Video's FEC; other classes keep their persisted policy
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
Response — the full merged policy view, abbreviated
{
  "classes": [
    {
      "class": "Video",
      "reliability": "DeadlineBounded",
      "multipath": { "kind": "Fec", "overhead_pct": 25 },
      "priority": 2,
      "latency_budget_ms": 200
    },
    { "class": "Control", "...": "..." }
  ]
}
Position: request, then the injected + advertised view
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 }
}
POST/api/config/push · /api/config/rollout/*

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).

Push — sign the live config's policy as the next epoch (toml optional)
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"] }
}
Canary rollout — phase 1 targets only the named nodes
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": []
}
Then release fleet-wide, or abort — and the guard rails
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
Serving the dashboard: the browser UI is static files on disk (/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.

Esc
↑ ↓ navigateEnter — openEsc — close