Files
abrown38andClaude Opus 5.5 1bc9f9f63a Let the Reserve button pick another address of the lease's subnet
The Reserve dialog fills in the lease's address but lets it be changed;
POST /leases/{ip}/reserve takes an optional ip, steward lease reserve
takes --ip. The address must be in the lease's subnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 00:29:07 +01:00

8.1 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, leaving out those matching interfaces.exclude
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, "address_order": "sequential",
 "options": [{"option": "domain-name-servers", "value": "10.0.10.2"}]}

address_order is sequential (left out when it is), reverse or random (subnets.md).

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:random"], "pool_order": "listed",
 "address_order": "default", "lease_time": 0,
 "options": [{"option": "routers", "value": "10.0.10.1"}], "disabled": false, "comment": ""}

access is deny-list (the default, left out) or allow-list; allow and deny replace the lists, each [{"match": "aa:bb:cc:*", "comment": "Sonos"}] with a MAC address, a prefix or "id HEX" (subnets.md). pools and options replace the lists; the pools keep their order. A pool's :ORDER is its own address order. pool_order is listed or random; address_order is sequential, reverse, random or default (the settings'). Subnets come back with pools as {"start", "end", "order"}, and the defaults (listed, default) left out. 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", "lease_time", "disabled", "comment"}; ip and mac or client_id needed (with both, either matches); lease_time 0 is the subnet's
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, and last_seen when the client last answered a ping (left out if it never has); 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 {"ip", "hostname", "comment", "by"}, ip another address of the lease's subnet to reserve instead, by being mac, client_id or both (the default)
GET /events[?mac=&subnet=&interface=&limit=&details=1] the latest DHCP messages, newest first (the last 1000 are kept in memory), each with an id, direction (in from the client, out from Steward) and client_id. subnet (ID, name or prefix) keeps messages for its addresses, and those without an address on its interface. details=1 adds fields and options
GET /events/{id} one message with fields (header fields that are set) and options, each {"code", "name", "value"} in words

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=]