Skip to content

API Reference ​

Base URLs:

  • Through the web service (recommended): https://<host>/api or https://localhost:8443/api
  • Directly to the backend, from the host only: http://127.0.0.1:5001/api

Every route needs HTTP Basic, JWT bearer, API token or session cookie authentication (see Authentication) except POST /api/auth/login, POST /api/auth/refresh, GET /api/health, GET /api/client-setup, GET /api/client-setup/ca.crt, GET /api/ready, GET /health, GET /livez, GET /readyz and GET /metrics. The WebSocket authenticates with a single-use token (see WebSocket). A running backend lists every registered route at GET /api/docs (authenticated).

OpenAPI ​

GET /api/openapi.json (authenticated) is an OpenAPI 3.1 description of every route the running backend registers, built from its router: paths, methods, summaries, which routes need authentication, and in x-spm-token-scope the scope an API token needs (read, write, or none when no token may call it). The lists, settings and access-log routes carry request and response schemas; the others are described without schemas for now.

The same document is published with this site at openapi.json, for client generators and API explorers. It leaves out the runtime profiles (/debug/pprof), which the live document includes.

Most successful responses are {"status": "success", "data": ...}. The authentication endpoints (/api/auth/login, /api/auth/refresh, /api/ws-token, /api/logout, /api/change-password) return a flat object with status and the payload fields at the top level. Errors are {"status": "error", "detail": "..."}; a 500 also carries "code": "internal_error" and a generic detail, with the cause in the backend log.

Collections ​

Endpoints that return a collection put it under data and the pagination under meta:

json
{
  "status": "success",
  "data": [ ... ],
  "meta": { "total": 1234, "limit": 100, "offset": 0 }
}

total, limit and offset are also emitted at the top level. They are deprecated; read meta.

Contract version ​

Every response carries an X-API-Version header: the version of the request and response shapes, 1 at this commit. It changes only when a shape a client depends on changes, unlike version in /api/health, which is the release.

Authentication and health ​

MethodPathDescription
POST/api/auth/loginCheck credentials, return an access and a refresh JWT, or with X-SPM-Session: cookie set the HttpOnly session cookie (no auth)
POST/api/auth/refreshExchange a refresh token for a new pair (no auth)
POST/api/logoutRevoke the bearer or session-cookie token of the request (clearing the cookie), and the refresh token in an optional {"refresh_token": ...} body
POST/api/change-passwordChange the admin password
GET/api/ws-tokenIssue a single-use WebSocket token
GET/api/sessionWho is signed in, how (cookie, bearer, basic) and expires_at
GET/api/healthStatus, version, commit (no auth)
GET/api/client-setupWhat a device needs (the /setup page): proxy_ip, proxy_auth, inspection (bump, peek, off) and ca (sha256, not_after, or null) (no auth)
GET/api/client-setup/ca.crtThe SSL-bump CA certificate as secure-proxy-ca.crt, 404 until the proxy has one; nginx also serves it at /ca.crt on ports 8011 and 8443 (no auth)
GET/api/ready, /readyz200 when the database answers, 503 otherwise (no auth)
GET/health, /livez200 while the process runs (no auth)
GET/metricsPrometheus metrics (no auth; nginx does not proxy it)
GET/api/docsThe list of registered routes

Client groups ​

MethodPathDescription
GET/api/client-groupsEvery client group
POST/api/client-groupsCreate a group (Client groups)
GET/api/client-groups/{id}One group
PUT/api/client-groups/{id}Replace a group
DELETE/api/client-groups/{id}Delete a group
PUT/api/ip-blacklist/sources/{id}/scope, /api/domain-blacklist/sources/{id}/scopeWhether a source applies to every client (global) or only to the groups that use it (groups)

Lists ​

{list} is one of ip-blacklist, domain-blacklist, ip-whitelist, domain-whitelist. Details: Blacklist and exception API.

MethodPathDescription
GET/api/{list}List entries (limit 1 to 1000, default 100; offset; search)
POST/api/{list}Add an entry
DELETE/api/{list}/{id}Delete an entry
POST/api/ip-blacklist/bulk-delete, /api/domain-blacklist/bulk-deleteDelete entries by ID
DELETE/api/ip-blacklist/clear-all, /api/domain-blacklist/clear-allDelete every entry
GET/api/{list}/sourcesWhere the entries came from, one row per source
DELETE/api/{list}/sources/{id}Delete the entries one source added ({id} or unrecorded)
GET/api/{list}/exportEvery entry, one per line, as text
POST/api/blacklists/importImport into the IP or domain blacklist from a URL or pasted content
POST/api/blacklists/import-geoImport countries' IPv4 blocks into the IP blacklist
POST/api/ip-whitelist/import, /api/domain-whitelist/importAdd a pasted list to IP exceptions or Domain exceptions as one pasted-list source
GET/api/blacklists/enforcementWhat enforces each list, from the proxy's records
POST/api/ip-blacklist/import, /api/domain-blacklist/importRetired: 410 Gone, use /api/blacklists/import

Default-deny allowlist ​

The UI's Default-deny allowlist; the API path is egress-allowlist. Used only while the egress_default_deny setting is true (default false); the setting is changed through POST /api/settings. See Default-deny allowlist and Egress provider lists.

MethodPathDescription
GET/api/egress-allowlistList manual entries
POST/api/egress-allowlistAdd a manual entry ({"entry": "<ip, cidr or domain>", "include_subdomains": <optional, domains only>, "description": "<optional>"})
DELETE/api/egress-allowlist/{id}Delete a manual entry
POST/api/egress-allowlist/bulk-deleteDelete manual entries by ID
DELETE/api/egress-allowlist/clear-allDelete every manual entry
GET/api/egress-providersList built-in catalog and custom URL egress provider lists
PUT/api/egress-providers/{name}Enable or disable a provider list (confirm_shared: true required to enable a shared network)
POST/api/egress-providers/urlAdd and fetch a custom URL provider list ({"label": "...", "url": "..."})
DELETE/api/egress-providers/url/{id}Delete a custom URL provider list and its prefixes
POST/api/egress-providers/{name}/refreshFetch and validate the provider list now
POST/api/egress-providers/{name}/apply-heldApply a held update for a provider list

Logs and analytics ​

Details: Logs and Analytics API.

MethodPathDescription
GET/api/logsAccess log rows, with filters (source, outcome, rule, destination, internal traffic) and a cursor
GET/api/logs/exportThe same rows as streamed CSV
GET/api/logs/statsTotal, blocked and direct-IP counts, blocks by rule
GET/api/logs/timelineRequests and blocks per hour (hours, default 24) of the zone tz (default UTC)
POST/api/logs/clearDelete every log row
POST/api/logs/clear-oldDelete rows older than days (query parameter, default 30)
POST/api/counters/resetDelete every log row and reset the WAF's counters
GET/api/statusProxy reachability, version, today's request count, Squid version, CVE count (squid_cves) and whether that major version has an advisory table (cves_checked), update availability
GET/api/dashboard/summaryThe dashboard's data in one call (top_clients aggregated by network and recent_blocks masked when gdpr_mode is on)
GET/api/traffic/statisticsRequests and blocks per bucket (period = hour, day, week, month)
GET/api/clients/statisticsTop 50 clients (or /24 and /64 networks when gdpr_mode is on), then the internal sources
GET/api/clients/{ip}/detailsOne client's or /24 / /64 network CIDR's totals, top domains and recent requests; ?window=7d for the last 7 days, all (default) for every request
GET/api/domains/statisticsTop 50 destinations of the last 7 days with their requests and a status: Blocked when a request to it was blocked or the proxy enforces the name from the blocklist (exact name, not on the allowed list), Allowed otherwise
GET/api/analytics/shadow-itRequests of the last 7 days to known services (file sharing, messaging, AI, social)
GET/api/analytics/user-agentsRequests of the last 7 days by method and kind of service
GET/api/analytics/file-extensionsRequests of the last 7 days by file extension
GET/api/analytics/top-domainsTop 50 domains
GET/api/analytics/top-destinationsThe 20 hosts the clients asked for most in the last 7 days, internal sources left out, each with requests and blocked
GET/api/analytics/blocked-by-dayBlocked requests per day by rule, today and the 6 days before (tz, default UTC)
GET/api/analytics/coverageWhich protections are switched on now: list sizes, WAF rules, HTTPS inspection, alerts and the other access settings
GET/api/audit-logAudit log, newest first, 50 a page (limit up to 200). category = all, changes or signins and q (text in the user, action, target or details) filter on the server; meta.retention_days is 365
GET/api/security/scoreRetired: 410 Gone
GET/api/security/cveThe Squid version (version), whether its major version has an advisory table (cves_checked) and the advisories matched to it (cves)
GET/api/waf/statsWAF statistics (from the WAF)
GET/api/waf/statusWhat the WAF runs: engine, block threshold, regex engine, policy mode, rule counts
GET/api/waf/categoriesWAF rule categories and whether each is on
POST/api/waf/categories/toggleTurn a category on or off in the running WAF
GET/api/waf/heuristicsThe heuristics the running WAF applies, or available: false with the reason
POST/api/waf/test-ruleMatch a Go regular expression against the destinations of recent requests (at most 10,000 read)
GET/api/waf/events/{id}What the WAF reported about one block, by the event_id of its access log row: rules, the ones that added to a score (scoring_rules), categories, host, path, blocked_by (spm or crs), scores and, for a Core Rule Set block, the argument each rule matched (crs_targets). 404 once pruned with the access log
GET/api/waf/policyThe WAF policy file, its sha256, and the policy the WAF has in force (waf, in_force)
PUT/api/waf/policyReplace the policy file with {policy, sha256}, sha256 being the digest GET returned; 409 when the file changed since
POST/api/waf/policy/exceptionsAdd one exception {rule, host?, path?, target?, comment?}
POST/api/waf/policy/observe-rulesLeave one rule {rule} to log only
GET/api/waf/trajectoriesClient trajectories that closed in the last days (1 to 90, default 7), latest first: client_ip, rule (order or breadth), start, closed (when the pattern was complete), end, phases, categories, hosts, signals
GET/api/waf/trajectories/{id}One trajectory and the client's signals in its window (start to 24 hours later), each with at, category, phase, host, rule, observed (Log only) and blocked; at most 500, truncated when there were more

Settings and maintenance ​

Details: Settings and Maintenance API.

MethodPathDescription
GET/api/settingsEvery setting; secrets are write-only
PUT/api/settings/{name}Set one value and write the proxy file it needs
POST/api/settingsStore several values and write the proxy files they drive
GET/api/database/sizeDatabase size (size_bytes, size_mb)
GET/api/database/statsRow count per table
GET/api/database/exportEvery table as JSON, password, secret and token columns redacted; client addresses in proxy_logs are unmasked (contains_unmasked_addresses: true when gdpr_mode is on)
POST/api/database/optimizeVACUUM and REINDEX
POST/api/database/resetDelete every row of every table except users
GET/api/cache/statisticsSquid cache statistics as the proxy last collected them
POST/api/maintenance/reload-configExport the lists, ask the proxy to regenerate and reload, and report what it did
POST/api/maintenance/reload-dnsExport every list again and ask dnsmasq to reload; says how many domain entries the DNS sinkhole got
POST/api/maintenance/clear-cacheClear the Squid cache (see Settings and Maintenance)
GET/api/maintenance/backup-configThe settings and the five lists, as a backup
POST/api/maintenance/restore-configAdd what a backup holds
GET/api/maintenance/check-cert-securityWhether the CA certificate and the certificate database exist, and whether the CA key is out of /config
GET/api/security/download-caThe SSL-bump CA certificate (/config/ssl_cert.pem)
GET/api/security/rate-limitsFailed-login counts per address
DELETE/api/security/rate-limits/{ip}Clear one address's count
POST/api/notifications/testSend one test alert to every channel that has all its settings (webhook, Gotify, Telegram, ntfy, Teams), outside the notification limits; data.channels reports each delivery
GET/api/notifications/statusFor each channel used since the backend started (since, kept in memory): last_attempt, last_delivered, last_error and last_error_at (why the last failed delivery failed, without the URL), last_failed, delivered, failed. Test alerts count

Internal ​

MethodPathDescription
POST/api/internal/alertWAF block notifications, on the backend's port only (backend:5000 inside the stack); the web container answers 404 for /api/internal/. With INTERNAL_ALERT_TOKEN set, only Authorization: Bearer <that token> is accepted; without it, normal admin authentication. The alerts pass the notification limits
POST/api/internal/signalsThe WAF's client signals (the categories in the client trajectory phases), stored and never sent to a channel. Same port and token as /api/internal/alert; the WAF's calls with the token are not counted by the per-address rate limit. Nothing is stored while gdpr_mode is on
POST/api/dns/detectProbe a subnet for Pi-hole or AdGuard instances (at most 2 scans at a time)
GET/debug/pprof/...Go runtime profiles (authenticated)

WebSocket ​

PathDescription
/api/ws/logsLive log feed; offer the subprotocols spm.v1 and spm-ws-token.<token> with a token from /api/ws-token. See WebSocket

Released under the MIT License. · Privacy & legal