Files
steward/docs/api.md
T
abrown38andClaude Opus 5.5 6ad1bb9a0d
release / release (push) Successful in 5m53s
Disable reservations; subnet enable/disable buttons; page-head CSS fix
- A disabled reservation is kept but ignored: the client gets a pool
  address like any other (steward reservation disable|enable, buttons and
  a checkbox in the web UI). Schema migration 3.
- Subnets get Disable/Enable buttons on their page and in the list.
- A rule left without its selector in style.css broke the page heading's
  layout, so the New ... buttons sat cramped under the description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:02:15 +01:00

6.6 KiB

The REST API

Everything the web UI and the CLI do goes through this API, under /api/v1. Requests and answers are JSON.

Authentication

  • API tokens: Authorization: Bearer steward_.... Make one in the web UI (your account), with steward token create NAME, or with stewardd user token USER NAME on the server.
  • Sessions: POST /auth/login with {"username", "password"} sets a cookie and returns csrf_token. Requests other than GET must send it as X-CSRF-Token.
  • The local socket (/run/steward/steward.sock): root and stewardd's own user need nothing; others send a token.

Failed logins and token attempts are rate limited per address and per user name. permissions.md has what each call needs.

Errors

{"error": {"code": "validation", "message": "subnet iot (10.0.20.0/24): it overlaps subnet office (10.0.10.0/24)",
           "problems": [{"severity": "error", "code": "subnet_overlap", "message": "...", "object": "subnet iot (10.0.20.0/24)"}]}}
Status Code
400 bad_request the request can't be read (an unknown option name, a bad address)
401 unauthenticated, session_expired, login_failed
403 forbidden, csrf
404 not_found
409 conflict e.g. undoing a change after the objects changed again
422 validation the change would make the configuration invalid; problems lists why
429 rate_limited

Changes

Calls that change the configuration answer:

{"applied": true, "history_id": 7,
 "changes": [{"kind": "subnet", "id": 12, "label": "iot (10.0.20.0/24)", "before": {...}, "after": {...}}],
 "warnings": [{"severity": "warning", "code": "interface_unknown", "message": "...", "object": "..."}],
 "object": {...}}
  • ?dry_run=1 checks the change and returns what it would do, with "applied": false, "dry_run": true.
  • before is missing for a create and after for a delete.
  • warnings are the problems the change brings; older ones are in /status.
  • object is the created or changed object.

Creating answers 201, other changes 200.

Endpoints

Status

GET /healthz {"ok": true, "version"}, without authentication
GET /status build, start time, interfaces (listening, error, addresses, subnets), subnets with usage, problems, leases (counts by state), messages (counts by type), ping_problem
GET /interfaces the host's interfaces, their addresses and subnets
GET /catalog the option catalog, value types, reserved codes, roles

Settings

GET /settings, PATCH /settings with any of:

{"lease_time": 43200, "ping_check": true, "ping_timeout_ms": 500, "decline_hold": 86400,
 "not_authoritative": false, "options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}

options replaces the list. Each option is {"option": NAME or NUMBER, "value": TEXT}, with "type" for numbers the catalog doesn't know (options.md).

Subnets

GET /subnets every subnet, with usage (pool_size, leased, declined, reservations, reservations_leased)
POST /subnets create; needs prefix and interface
GET /subnets/{ref} by ID, name, or prefix with _ for / (10.0.10.0_24)
PATCH /subnets/{ref} change the given fields
DELETE /subnets/{ref} delete, with its reservations
{"name": "office", "interface": "eth0", "prefix": "10.0.10.0/24",
 "pools": ["10.0.10.100-10.0.10.199", "10.0.10.224/28"], "lease_time": 0,
 "options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}

pools and options replace the lists; lease_time 0 means the settings'.

Reservations

GET /reservations[?subnet=REF] each with subnet_id, subnet and its current lease
POST /reservations {"ip", "mac", "client_id", "hostname", "options", "disabled", "comment"}; ip and mac or client_id needed
GET, PATCH, DELETE /reservations/{ref} by ID, address, or MAC address (when it has one reservation)

Leases and activity

GET /leases[?subnet=&state=&interface=&q=] every lease, with its state now (active, expired, released, declined), subnet and its reservation; q searches address, MAC and host name
GET /leases/{ip} one
DELETE /leases/{ip} forget it (audited)
POST /leases/{ip}/reserve make it a reservation; optional {"hostname", "comment"}
GET /events[?mac=&interface=&limit=] the latest DHCP messages, newest first (the last 1000 are kept in memory)

History and import

GET /history[?limit=&before=] changes, newest first
GET /history/{id} one, with each object before and after
POST /history/{id}/revert undo it; 409 if an object it touched has changed since, or it was undone already
POST /import/kea {"config": TEXT, "leases": [TEXT, ...], "interfaces": {"PREFIX": "IFACE"}, "replace": false} (kea.md); answers with leases, notes, skipped too

Certificate

Administrators only (certificates.md).

GET /certificate {"configured", "source": "acme"|"file"|"none", "status": {"domain", "directory", "staging", "state": "none"|"ok"|"working"|"error", "not_before", "not_after", "issuer", "renew_at", "last_error", "last_attempt", "next_attempt", "check"}}
POST /certificate/check run the check now (up to a few minutes); answers {"time", "domain", "ok", "steps": [{"name", "status", "detail", "queries"}]}
POST /certificate/renew get a new certificate now, in the background (202); follow it with GET /certificate

GET /status lists the certificate's problems (certificate_failed, certificate_expiring, certificate_staging) for administrators.

Users, tokens, audit

POST /auth/login, POST /auth/logout, GET /auth/whoami whoami has token_id when the caller uses a token
POST /me/password {"current", "password"}
GET /me/tokens, POST /me/tokens {"name", "role", "grants", "expires_in"}; the answer's secret is shown once
GET /tokens, DELETE /tokens/{id} all tokens (administrators), revoke
POST /tokens/{id}/rotate a new secret: {"grace": "1h", "expires_in": "90d", "no_expiry": false}, all optional; answers with secret and previous_secret_valid_until
GET, POST /users; PATCH, DELETE /users/{id} administrators; {"username", "password", "admin", "role", "grants", "disabled", "comment"}
GET, POST /users/{id}/tokens
GET /audit[?user=&action=&result=&q=&before=&limit=]