Skip to content

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.

FactValue
API version26.06.1
API specOpenAPI 3.1
Schema validationPydantic 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.

When ENABLE_DOCS=true is set in a non-production environment, two interactive UIs are available:

UIPath
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.

These endpoints sit outside /api/v1 and require no authentication:

MethodPathPurpose
GET/healthContainer orchestration probe - returns `{“status”:“healthy"
GET/Root info: {app, docs, api}

A richer health router is also mounted inside the API:

MethodPathPurpose
GET/api/v1/healthAggregated subsystem health
GET/api/v1/health/liveLiveness probe
GET/api/v1/health/readyReadiness probe (gated by READINESS_STRICT_DEPS)
GET/api/v1/health/dbDatabase connectivity

Every data endpoint requires an authenticated caller. Three credential types are accepted:

  1. JWT Bearer token - obtained from POST /api/v1/auth/login (30-minute access token, 7-day refresh token).
  2. httpOnly cookie (freesdn_access) - set automatically by the browser login flow; used by the web UI.
  3. API key (X-API-Key header) - long-lived, scoped credential for service accounts and automation.

Authorization is enforced through a 7-tier role hierarchy:

RoleLevel
super_admin100
admin80
org_admin60
site_admin40
operator20
viewer10
guest0

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.

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.

LimitDefaultFailure mode
General API600 requests / minute per principalFail-open (request proceeds if Valkey is unreachable)
Auth endpoints (/api/v1/auth/*)5 requests / minute per IPFail-closed - returns 503 if Valkey is unreachable
Per-user sliding window (credential stuffing)20 failed attempts / 5 minutesIn-memory fallback (avoids remote lockout)
Burst120 requests / second429 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.

All requests are capped at 1 MiB. Larger bodies return 413 Request Entity Too Large. This backstop guards staged-write payloads and file uploads.

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.

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).

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.

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.

Most records carry a deleted_at timestamp. DELETE endpoints soft-delete the row rather than removing it. Queries filter deleted_at IS NULL.

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.

Every response includes:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Cache-Control: no-store, no-cache, must-revalidate
Content-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).

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"}
]
}
}
ExceptionHTTP statuserror.type
Adapter connection failure502adapter_connection_error
Adapter authentication failure502adapter_authentication_error
Adapter not found404adapter_not_found
Adapter timeout504adapter_timeout
Adapter rate limit429adapter_rate_limit
Adapter generic502adapter_error
Validation error422(details array)
Unhandled exception500generic message; full detail logged, not leaked

Each of the 10 loaded modules mounts at /api/v1/{id}/:

ModuleRoute prefixNotes
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
StorageNo HTTP routesFabric participant only

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.

MethodPathPurpose
POST/api/v1/auth/loginJSON login - returns tokens or MFA challenge
POST/api/v1/auth/login/mfaComplete a TOTP MFA challenge
POST/api/v1/auth/tokenOAuth2 password grant (refuses MFA-enabled accounts)
POST/api/v1/auth/refreshRotate refresh token
GET/api/v1/auth/meCurrent user info
POST/api/v1/auth/logoutPer-device logout
POST/api/v1/auth/logout-allInvalidate all sessions (bumps token_version)
GET/api/v1/auth/sessionsList active sessions
DELETE/api/v1/auth/sessions/{id}Revoke a session
POST/api/v1/auth/mfa/setupBegin TOTP enrolment
POST/api/v1/auth/mfa/enableConfirm TOTP enrolment
POST/api/v1/auth/mfa/disableDisable MFA
POST/api/v1/auth/password/reset-requestRequest password reset link
POST/api/v1/auth/password/resetConsume reset token

Self-registration (POST /api/v1/auth/register) is gated by ALLOW_REGISTRATION (default false).

MethodPathPurpose
GET/api/v1/auth/sso/providers/publicProvider buttons for the login page (requires organization_slug)
POST/api/v1/auth/sso/oidc/authorizeStart OIDC auth-code flow
POST/api/v1/auth/sso/oidc/callbackExchange code for tokens
POST/api/v1/auth/sso/ldap/authenticateLDAP bind
GET/POST/PATCH/DELETE/api/v1/auth/sso/providersProvider CRUD (admin)
MethodPathPurpose
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).

PrefixMin 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.

PrefixNotes
/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.

PrefixNotes
/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.

PrefixNotes
/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
PrefixNotes
/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
PrefixNotes
/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

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:

VendorPrefixes
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
MikroTikfirewall, interfaces, ip, dhcp, dns, vpn, routing, queues, ppp, hotspot, capsman, security, system
OpenWrtbase, firewall, dhcp
Proxmoxvm, container, snapshot, storage, backup, cluster, ha, node, replication, sdn, ceph, firewall
UniFiTwo parallel families - adapter_unifi_* (gateway-style) and unifi_* (REST surface at /unifi/)

Write operations that mutate live network devices go through a dual-gate staged-write pipeline:

  1. A mutation call (POST / PUT / PATCH / DELETE) writes a pending change to the database. It does not touch the live device.
  2. An explicit POST /api/v1/gateway-vpn/changes/{change_id}/apply with {"force": true} in the request body and both ADAPTER_READ_ONLY=false and OMADA_READ_ONLY=false set in the environment pushes the change to the device. Either flag left at its default of true keeps 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 the feature field stored in the pending-change record. (The sites/{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.

Real-time events (device state changes, alerts, job progress, scan results) are delivered over WebSocket.

PathPurpose
WS /api/v1/wsReal-time event stream
GET /api/v1/ws/statsActive 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.

/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.

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.

The following environment variables affect API behavior. See Configuration Reference for the full list.

VariableDefaultEffect
ENABLE_DOCStrueEnable Swagger / ReDoc (blocked in ENVIRONMENT=production)
ENABLE_METRICStrueEnable /metrics
METRICS_AUTH_TOKEN(none)Require Bearer token for /metrics; fail-closed in prod when unset
ADAPTER_READ_ONLYtrueAll vendor writes are staged only
OMADA_READ_ONLYtrueOmada writes are staged only
ALLOW_REGISTRATIONfalsePublic self-registration endpoint
RATE_LIMIT_RPM600Per-principal sustained requests / minute
RATE_LIMIT_BURST120Burst requests / second
STRICT_STARTUPfalseAbort boot on module or event-bus failure
READINESS_STRICT_DEPSfalseValkey / LogDB absence causes 503 on /health/ready

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 501 if the pyotp Python 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 hardcoded DEFAULT_ROLE_PERMISSIONS map keyed on user.role. Per-user overrides and stored role rows are not yet merged into the auth decision.
  • JWT role claim is a display hint. Authorization always reads user.role from 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_* and adapter_unifi_*) coexist; routing is by distinct prefix. This is a known layering debt.