> Source: https://nexusatlas.dev/docs/api — Nexus Atlas developer documentation (Reference). Converted from the HTML page; the page is canonical.

# 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 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 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

### 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](https://nexusatlas.dev/docs/concepts/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](https://nexusatlas.dev/docs/config).

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](https://nexusatlas.dev/docs/sdk). Until then, every response shape on this page is verified against the daemon source, and the [config schema](https://nexusatlas.dev/docs/config) is already machine-readable today.
