API Overview
FreeSDN’s REST API is the primary integration surface. Every action the web UI performs - reading device state, staging a configuration change, triggering a scan - goes through this API. You can drive it directly from scripts, CI pipelines, the freesdn-agent, or any HTTP client.
Structure
Section titled “Structure”| Fact | Value |
|---|---|
| API version | 26.06.1 |
| API spec | OpenAPI 3.1 |
| Schema validation | Pydantic v2 |
| Base path | /api/v1 |
All module routes mount at /api/v1/{module-id}/. Core platform routes (auth, users, devices, discovery, automation, fabric, etc.) mount at /api/v1/ directly. The vendor adapter “gateway” surface - the staged-write endpoints that talk to real hardware - uses prefixes like /api/v1/gateway-vpn/, /api/v1/gateway-firewall/, etc.
Interactive documentation
Section titled “Interactive documentation”When ENABLE_DOCS=true is set in a non-production environment, two interactive UIs are available:
| UI | Path |
|---|---|
| Swagger UI | /api/v1/docs |
| ReDoc | /api/v1/redoc |
| OpenAPI JSON | /api/v1/openapi.json |
During development, any non-production tier serves the docs. Start the stack and open http://localhost:8000/api/v1/docs.
Non-versioned health and root endpoints
Section titled “Non-versioned health and root endpoints”These endpoints sit outside /api/v1 and require no authentication:
| Method | Path | Purpose |
|---|---|---|
GET | /health | Container orchestration probe - returns `{“status”:“healthy" |
GET | / | Root info: {app, docs, api} |
A richer health router is also mounted inside the API:
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/health | Aggregated subsystem health |
GET | /api/v1/health/live | Liveness probe |
GET | /api/v1/health/ready | Readiness probe (gated by READINESS_STRICT_DEPS) |
GET | /api/v1/health/db | Database connectivity |
Authentication and authorization
Section titled “Authentication and authorization”Every data endpoint requires an authenticated caller. Three credential types are accepted:
- JWT Bearer token - obtained from
POST /api/v1/auth/login(30-minute access token, 7-day refresh token). - httpOnly cookie (
freesdn_access) - set automatically by the browser login flow; used by the web UI. - API key (
X-API-Keyheader) - long-lived, scoped credential for service accounts and automation.
Authorization is enforced through a 7-tier role hierarchy:
| Role | Level |
|---|---|
super_admin | 100 |
admin | 80 |
org_admin | 60 |
site_admin | 40 |
operator | 20 |
viewer | 10 |
guest | 0 |
Roles are combined with a hybrid per-user site-grant model. Queries are org-scoped in the service layer. A user can only see resources belonging to their own organization unless they are super_admin.
For the full permission matrix and site-grant model, see Roles and Permissions.
CSRF protection
Section titled “CSRF protection”The browser login flow sets a freesdn_csrf cookie (not httpOnly) at path /. The SPA reads it via document.cookie and sends it back as the X-CSRF-Token request header on all state-mutating calls. The server compares cookie and header values with a constant-time comparison.
Safe methods (GET, HEAD, OPTIONS) skip CSRF validation. Public endpoints (login, password reset, SSO callbacks) are explicitly exempt.
If you are calling the API programmatically using an API key with no session cookie, CSRF is not required - the check is skipped when no freesdn_access cookie is present. If both a cookie and an API key or Bearer token are sent simultaneously, CSRF is still enforced.
Rate limiting
Section titled “Rate limiting”| Limit | Default | Failure mode |
|---|---|---|
| General API | 600 requests / minute per principal | Fail-open (request proceeds if Valkey is unreachable) |
Auth endpoints (/api/v1/auth/*) | 5 requests / minute per IP | Fail-closed - returns 503 if Valkey is unreachable |
| Per-user sliding window (credential stuffing) | 20 failed attempts / 5 minutes | In-memory fallback (avoids remote lockout) |
| Burst | 120 requests / second | 429 Too Many Requests, Retry-After: 1 |
Rate-limit state is keyed on a signature-verified JWT subject (no database lookup). Response headers include X-RateLimit-Limit and X-RateLimit-Remaining.
Exceeded limits return HTTP 429. Health probe paths (/health, /api/v1/health*) and camera stream-token endpoints bypass rate limiting.
Request and response conventions
Section titled “Request and response conventions”Body size
Section titled “Body size”All requests are capped at 1 MiB. Larger bodies return 413 Request Entity Too Large. This backstop guards staged-write payloads and file uploads.
Trailing slashes
Section titled “Trailing slashes”Both /sites and /sites/ route to the same handler. A middleware layer normalizes trailing slashes without issuing a redirect, avoiding the internal-proxy-host leak that a 307 would expose.
Pagination
Section titled “Pagination”List endpoints return a PaginatedResponse envelope:
{ "items": [...], "total": 142, "page": 1, "per_page": 20, "pages": 8}Pass page and per_page as query parameters. Caps vary by endpoint (for example, per_page is capped at 200 for users and 100 for sites).
Site filtering
Section titled “Site filtering”Pass site_id as a query parameter on any endpoint that supports it. The server enforces org membership and per-user site grants server-side - passing a site ID from another organization returns an empty result or 404, not a data leak.
Search
Section titled “Search”Endpoints that accept a search query parameter use a case-insensitive ILIKE with special characters escaped, so % and _ in the search string are treated as literals.
Soft delete
Section titled “Soft delete”Most records carry a deleted_at timestamp. DELETE endpoints soft-delete the row rather than removing it. Queries filter deleted_at IS NULL.
Request ID
Section titled “Request ID”Send an X-Request-ID header (pattern ^[A-Za-z0-9\-_]{1,128}$) to correlate requests across logs. The server echoes it back. Client-supplied IDs are prefixed ext- in server logs to flag potential log-injection attempts.
Security headers
Section titled “Security headers”Every response includes:
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originCache-Control: no-store, no-cache, must-revalidateContent-Security-Policy: default-src 'self'; script-src 'self'; ...Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()HSTS is added when the request arrives over HTTPS (detected via X-Forwarded-Proto or the scheme).
Error shape
Section titled “Error shape”Most route error responses share a common JSON envelope:
{ "error": { "code": 403, "message": "Permission denied: requires 'device:write'", "request_id": "ext-abc123" }}Exceptions are responses produced by ASGI middleware layers that run before exception handlers:
- 429 (burst) -
{"detail": "Rate limit exceeded (burst)", "retry_after": 1} - 429 (per-minute) -
{"detail": "Rate limit exceeded", "retry_after": 60} - 413 -
{"detail": "request body exceeds 1048576-byte limit"}
These use FastAPI’s default {"detail": ...} format, not the {"error": {...}} envelope.
Validation errors (422) include a details array:
{ "error": { "code": 422, "message": "Validation error", "request_id": "...", "details": [ {"field": "per_page", "message": "ensure this value is less than or equal to 100", "type": "value_error"} ] }}| Exception | HTTP status | error.type |
|---|---|---|
| Adapter connection failure | 502 | adapter_connection_error |
| Adapter authentication failure | 502 | adapter_authentication_error |
| Adapter not found | 404 | adapter_not_found |
| Adapter timeout | 504 | adapter_timeout |
| Adapter rate limit | 429 | adapter_rate_limit |
| Adapter generic | 502 | adapter_error |
| Validation error | 422 | (details array) |
| Unhandled exception | 500 | generic message; full detail logged, not leaked |
Module route-prefix map
Section titled “Module route-prefix map”Each of the 10 loaded modules mounts at /api/v1/{id}/:
| Module | Route prefix | Notes |
|---|---|---|
| Network Management | /api/v1/network/ | Reference adapter: Omada |
| Video Surveillance | /api/v1/cameras/ | - |
| VoIP & Telephony | /api/v1/voip/ | - |
| Firewall (+ Gateway orchestration) | /api/v1/firewall/ | Absorbs former Gateway module |
| Access Control | /api/v1/access_control/access/ | BETA, off by default |
| Configuration Backup | /api/v1/backup/ | v1.1.0 |
| AI Assistant | /api/v1/ai/ | BETA |
| Observability | /api/v1/collector/ | Formerly “Collector” |
| Compute / Hypervisor | /api/v1/hypervisor/ | Proxmox VE |
| Storage | No HTTP routes | Fabric participant only |
Core platform route groups
Section titled “Core platform route groups”These routes are part of the base API and are not tied to a specific module. This is a representative map, not an exhaustive list.
Auth and identity
Section titled “Auth and identity”| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/auth/login | JSON login - returns tokens or MFA challenge |
POST | /api/v1/auth/login/mfa | Complete a TOTP MFA challenge |
POST | /api/v1/auth/token | OAuth2 password grant (refuses MFA-enabled accounts) |
POST | /api/v1/auth/refresh | Rotate refresh token |
GET | /api/v1/auth/me | Current user info |
POST | /api/v1/auth/logout | Per-device logout |
POST | /api/v1/auth/logout-all | Invalidate all sessions (bumps token_version) |
GET | /api/v1/auth/sessions | List active sessions |
DELETE | /api/v1/auth/sessions/{id} | Revoke a session |
POST | /api/v1/auth/mfa/setup | Begin TOTP enrolment |
POST | /api/v1/auth/mfa/enable | Confirm TOTP enrolment |
POST | /api/v1/auth/mfa/disable | Disable MFA |
POST | /api/v1/auth/password/reset-request | Request password reset link |
POST | /api/v1/auth/password/reset | Consume reset token |
Self-registration (POST /api/v1/auth/register) is gated by ALLOW_REGISTRATION (default false).
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/auth/sso/providers/public | Provider buttons for the login page (requires organization_slug) |
POST | /api/v1/auth/sso/oidc/authorize | Start OIDC auth-code flow |
POST | /api/v1/auth/sso/oidc/callback | Exchange code for tokens |
POST | /api/v1/auth/sso/ldap/authenticate | LDAP bind |
GET/POST/PATCH/DELETE | /api/v1/auth/sso/providers | Provider CRUD (admin) |
API keys
Section titled “API keys”| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/api-keys/ | List your keys |
POST | /api/v1/api-keys/ | Create a key (secret returned once) |
DELETE | /api/v1/api-keys/{key_id} | Revoke a key |
Scopes are an explicit permission ceiling. You cannot grant scopes beyond your own permissions. A user is limited to 50 active keys. Each key accepts name, description, scopes (up to 32 entries), and expires_in_days (1-365).
Users, organizations, and sites
Section titled “Users, organizations, and sites”| Prefix | Min role |
|---|---|
/api/v1/users/ | admin |
/api/v1/organizations/ | admin / org_admin |
/api/v1/sites/ | org_admin (write), authenticated (read) |
/api/v1/roles/ | admin |
/api/v1/credentials/ | settings:read / settings:write |
Role assignment is strictly lower-than: you can only assign a role below your own level. There is a last-admin guard that prevents you from removing the last admin in an org.
Devices and discovery
Section titled “Devices and discovery”| Prefix | Notes |
|---|---|
/api/v1/devices/ | Managed device registry |
/api/v1/controllers/ | Per-vendor controller CRUD + multi-controller ops |
/api/v1/discovery/ | Async 4-phase scan pipeline |
Discovery requires the discovery:run permission. Adopting a discovered device requires discovery:write. Active scan concurrency is capped server-side; exceeding it returns 429.
Automation and Fabric
Section titled “Automation and Fabric”| Prefix | Notes |
|---|---|
/api/v1/automation/ | Rules and schedules |
/api/v1/fabric/ | Universal app-interconnect catalog and connections |
/api/v1/webhooks/ | Outbound webhook subscriptions |
/api/v1/events/ | Event bus read and replay |
/api/v1/actions/ | - |
GET /api/v1/fabric/catalog returns the tier-tagged catalog of operations, events, and AI-tool projections for both native modules and plugins.
Network infrastructure
Section titled “Network infrastructure”| Prefix | Notes |
|---|---|
/api/v1/switches/ | Port, VLAN, LAG, mirroring |
/api/v1/access-points/ | WiFi, RF health, rogue AP |
/api/v1/network/ | VLAN alignment, distribution |
/api/v1/poe/ | PoE control per port |
/api/v1/vpn/ | IPsec / OpenVPN / WireGuard (59 core + 10 orchestration) |
/api/v1/topology/ | Network graph |
/api/v1/dpi/ | Deep packet inspection / traffic analytics |
/api/v1/radius/ | 802.1X / RADIUS |
/api/v1/ztp/ | Zero-touch provisioning |
Enterprise and observability
Section titled “Enterprise and observability”| Prefix | Notes |
|---|---|
/api/v1/analytics/ | Traffic and usage analytics |
/api/v1/enterprise/ | Enterprise configuration |
/api/v1/sla/ | SLA monitoring and reports |
/api/v1/alert-rules/ | Alert rule CRUD |
/api/v1/audit/ | Audit log read |
/api/v1/security/ | Security events |
/api/v1/correlation/ | Event correlation |
/api/v1/logs/ | Log query |
/api/v1/notifications/ | Notification channels |
Agents, plugins, and marketplace
Section titled “Agents, plugins, and marketplace”| Prefix | Notes |
|---|---|
/api/v1/agents/ | Desktop / headless agent fleet |
/api/v1/plugins/ | Plugin lifecycle |
/api/v1/marketplace/plugins/ | Ed25519-signed catalog |
/api/v1/integrations/ | Third-party integrations |
Vendor adapter gateway surface
Section titled “Vendor adapter gateway surface”The vendor adapter surface is a large segment of the API. These routes handle staged writes and reads against real hardware (Omada, OPNsense, pfSense, MikroTik, OpenWrt, Proxmox, UniFi). The staging URL pattern differs by vendor:
Omada (site-scoped - Omada’s API nests resources under a site context):
/api/v1/gateway-{area}/{controller_id}/sites/{site_id}/changes/{feature}?operation=...All other vendors - OPNsense, pfSense, MikroTik, OpenWrt, Proxmox, UniFi (controller-scoped only):
/api/v1/gateway-{area}/{controller_id}/changes/{feature}?operation=...Representative prefixes:
| Vendor | Prefixes |
|---|---|
| Omada | /gateway-vpn/, /gateway-firewall/, /gateway-wifi/, /gateway-switch-advanced/, /gateway-firmware/, /gateway-routing/, /gateway-diagnostics/, /gateway-hotspot/, /gateway-raw/, /gateway-bulk/, /gateway-system/, /gateway-profiles/, /gateway-insights/, /gateway-openapi/ |
| OPNsense | /gateway-opnsense-{area}/ - firewall, nat, dhcp, dns, vpn, routing, services, ids, shaper, system, diagnostics, interfaces, cron |
| pfSense | /gateway-pfsense-{area}/ - firewall, nat, dhcp, dns, vpn, routing, services, system, diagnostics, interfaces |
| MikroTik | firewall, interfaces, ip, dhcp, dns, vpn, routing, queues, ppp, hotspot, capsman, security, system |
| OpenWrt | base, firewall, dhcp |
| Proxmox | vm, container, snapshot, storage, backup, cluster, ha, node, replication, sdn, ceph, firewall |
| UniFi | Two parallel families - adapter_unifi_* (gateway-style) and unifi_* (REST surface at /unifi/) |
Staged writes
Section titled “Staged writes”Write operations that mutate live network devices go through a dual-gate staged-write pipeline:
- A mutation call (
POST/PUT/PATCH/DELETE) writes a pending change to the database. It does not touch the live device. - An explicit
POST /api/v1/gateway-vpn/changes/{change_id}/applywith{"force": true}in the request body and bothADAPTER_READ_ONLY=falseandOMADA_READ_ONLY=falseset in the environment pushes the change to the device. Either flag left at its default oftruekeeps writes staged. This is a single shared endpoint - every vendor adapter (Omada, OPNsense, pfSense, MikroTik, OpenWrt, Proxmox, UniFi) routes through the same/gateway-vpn/-prefixed apply path. The endpoint dispatches to the correct vendor applier based on thefeaturefield stored in the pending-change record. (Thesites/{site_id}segment appears in Omada staging URLs but not in non-Omada staging URLs and not in the apply URL - the apply endpoint looks up the controller and site from the stored change record regardless of vendor.)
Both gates must pass. Either gate missing → the apply call returns an error without touching the device.
For catastrophic operations (VM destroy, firmware flash, factory reset, config restore, etc.), an additional role gate applies both at stage time and at apply time - you must hold at least site_admin to even queue those changes. This closes the queue-poisoning window where a lower-tier principal stages a destructive change for a higher-tier principal to inadvertently apply.
See Using the API for a worked example of the stage → inspect → apply flow.
WebSocket
Section titled “WebSocket”Real-time events (device state changes, alerts, job progress, scan results) are delivered over WebSocket.
| Path | Purpose |
|---|---|
WS /api/v1/ws | Real-time event stream |
GET /api/v1/ws/stats | Active connection count |
The recommended auth approach is to send an auth frame as the first message after connecting:
{"type": "auth", "token": "<access_token>"}Per-connection caps: 25 connections per user, 5,000 global, 200 subscriptions per connection. Exceeded caps close the socket with code 1013 Try Again Later. Cross-pod targeted delivery is handled through Valkey pubsub.
Prometheus metrics
Section titled “Prometheus metrics”/metrics exposes Prometheus-format metrics. This path:
- Is only served when
ENABLE_METRICS=true. - Requires
Authorization: Bearer <METRICS_AUTH_TOKEN>in production. Without the token set, the endpoint is not served. - Is internal-only in the default Docker Compose setup - no host port is published.
Setup wizard
Section titled “Setup wizard”POST /api/v1/setup/* routes are public (no JWT required). They support the initial admin account and organization creation flow. Once setup is complete, these endpoints become effectively inert for re-initialization.
Key configuration variables
Section titled “Key configuration variables”The following environment variables affect API behavior. See Configuration Reference for the full list.
| Variable | Default | Effect |
|---|---|---|
ENABLE_DOCS | true | Enable Swagger / ReDoc (blocked in ENVIRONMENT=production) |
ENABLE_METRICS | true | Enable /metrics |
METRICS_AUTH_TOKEN | (none) | Require Bearer token for /metrics; fail-closed in prod when unset |
ADAPTER_READ_ONLY | true | All vendor writes are staged only |
OMADA_READ_ONLY | true | Omada writes are staged only |
ALLOW_REGISTRATION | false | Public self-registration endpoint |
RATE_LIMIT_RPM | 600 | Per-principal sustained requests / minute |
RATE_LIMIT_BURST | 120 | Burst requests / second |
STRICT_STARTUP | false | Abort boot on module or event-bus failure |
READINESS_STRICT_DEPS | false | Valkey / LogDB absence causes 503 on /health/ready |
Honesty caveats
Section titled “Honesty caveats”The following limitations apply to this release:
- SAML SSO returns
501. Use OIDC or LDAP. - Access Control door operations return
501. No door adapter ships yet. - MFA setup returns
501if thepyotpPython package is not present in the container image. - Role permissions are static. The
/api/v1/roles/endpoints manage stored Role rows, but the authorization dependency resolves permissions from a hardcodedDEFAULT_ROLE_PERMISSIONSmap keyed onuser.role. Per-user overrides and stored role rows are not yet merged into the auth decision. - JWT
roleclaim is a display hint. Authorization always readsuser.rolefrom the database, never from the token. Any tooling that parses the JWT role claim for access decisions is relying on unverified data. - Two UniFi route families (
unifi_*andadapter_unifi_*) coexist; routing is by distinct prefix. This is a known layering debt.
Next steps
Section titled “Next steps”- Authentication - API keys, JWT flow, MFA, CSRF tokens
- Using the API - worked examples including staged writes and WebSocket
- Roles and Permissions - full permission matrix by role
- Configuration Reference - all environment variables
- Adapters - per-vendor capability and maturity matrix