Files
steward/docs/api.md
T
abrown38andClaude Opus 5.5 4fd72998ec Reservation lease time; reservations by MAC address or client ID
- A reservation can have its own lease time (--lease-time, the
  reservation dialog), over the subnet's. Schema migration 6.
- A reservation with a MAC address and a client ID matches either one
  (a dual-boot machine sends two client IDs from one MAC); before, it
  was found by MAC and then refused. A lease of the reserved address by
  the client's other identity counts as its own.
- The Leases page shows a client's client ID; reserving a lease uses
  both, or one with --by / the dialog's choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:26:24 +01:00

7.5 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": ""}

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 {"hostname", "comment", "by"}, by being mac, client_id or both (the default)
GET /events[?mac=&subnet=&interface=&limit=] the latest DHCP messages, newest first (the last 1000 are kept in memory). subnet (ID, name or prefix) keeps messages for its addresses, and those without an address on its interface

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