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.

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.

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.

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

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 5-tier role hierarchy:

Role Level
super_admin 100
org_admin 60
site_admin 40
operator 20
viewer 10

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.

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.

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"}
]
}
}
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

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/ Coming soon - disabled by default, cannot be enabled yet
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

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.

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

Prefix Min role
/api/v1/users/ org_admin
/api/v1/organizations/ admin / org_admin
/api/v1/sites/ org_admin (write), authenticated (read)
/api/v1/roles/ org_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.

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.

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.

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

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

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.

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.

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

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

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.

All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.