Flat reference of every route the panel exposes. Authoritative source: backend/internal/server/server.go — if drift shows up between this page and the binary, the code wins.
Response shape for errors is uniformly {"error":"<message>"} with an HTTP status in 4xx/5xx. Success responses are per-endpoint; listings return [...] arrays (never null), single-row GETs return an object.
Update (full replace; partial fields take a *bool shape).
DELETE
/api/hosts/{id}
Delete.
POST
/api/hosts/{id}/toggle
Toggle the enabled flag.
The host record carries (most relevant for v1.3.18+):
lan_only (v1.3.18) — when true, Caddy gates the host with a remote_ip matcher that returns 403 to public IPs.
true_detect_mode (v1.3.19, activated v1.3.29) — when true, the panel writes a profiles.yaml entry that suppresses LAPI decision creation for AppSec alerts whose target_fqdn matches this host. Useful for hosts whose legitimate traffic triggers AppSec false positives (socket.io polling, monitoring tools). See Per-host true_detect_mode and the v1.3.29 release notes for the profiles.yaml splice protocol.
tls_acme_ca_url (v1.3.7) — per-host override of the global ACME CA URL.
tls_challenge (v1.3.7) — dns / http / tls-alpn.
tls_dns_provider (v1.3+) — names the dns_providers row this host pulls credentials from when tls_challenge='dns'.
For the broader /api/security/* namespace (decisions, whitelist, scenarios management, drift detection, country bans), see the Security tabs section below.
List all operator-uploaded certs (metadata only; never the key).
GET
/api/manual-certs/{host_id}
One cert's metadata. 404 when the host has no manual cert.
POST
/api/manual-certs/{host_id}
Multipart upload: cert_pem, key_pem, optional chain_pem. Validates + encrypts the key + writes files to the shared volume + flips the host to tls_mode=manual.
DELETE
/api/manual-certs/{host_id}?revert=auto\|none
Remove the cert + files, revert the host's TLS mode (default auto).
GET
/api/manual-certs/{host_id}/download
Stream cert + chain PEM for inspection (key is never served).
See Manual certificates for validation semantics and the manual_cert_expiring_soon notification event.
Full surface under /api/logs/... — list, stream (SSE tail), stats, timeseries, single row. Wired via h.RouteLogsMux(r); the exact shape is in internal/api/logs.go.
Long time-only windows (v1.3.38.4): when the filter is only from/to (optionally source=caddy_access) and the window is longer than 24 h or unbounded, /api/logs/stats answers total, by_status_class, by_source and top_hosts from covering indexes over the whole window, while avg_duration_ms / p95_duration_ms are computed on the newest sample_n rows (20,000) and top_paths on the newest detail_window ("24h0m0s"). Both fields are absent when every figure covers the whole filter. /api/logs/timeseries at hourly buckets takes the same path (other = total minus the four classes). Any other filter (q, path, status, remote_ip, host_id, ...) keeps the row-visiting queries. Bridge until the v1.3.40 hourly rollup.
Log pipeline (v1.3.40.0):
Method
Path
Purpose
GET
/api/logs/pipeline
The effective policy with defaults applied: retention (caddy_access_days 7, caddy_error_days 30, audit_days 90, waf_audit_days 30, default_days, max_entries, raw_hours 24, raw_watermark), ingest (drop_loggers, drop_user_agents, drop_paths and dropped = today's counters per rule, since_boot, rules), current (rows by source, db_size_bytes, oldest) and estimate (rows and bytes the policy keeps at the newest-24-h traffic, per source; cached 5 min).
GET
/api/logs
adds notes[] when a free-text q reaches rows older than raw_hours: the raw JSON was searched only on the newest window, the columns on every row.
GET
/api/logs/{id}
adds raw_stripped: true and raw_note when the row's raw JSON was removed by the policy; every column is complete.
PUT
/api/settings/{key}
new keys logs.retention.<source>_days, logs.retention.raw_hours, logs.ingest.drop_loggers / drop_user_agents / drop_paths (comma-separated lists; a logger item may be logger|message prefix). Writing a logs.ingest.* key reloads the filter at once.
The ingest filter runs after the notification watcher (target up/down and WAF bursts are still detected on dropped lines) and before the writer; every drop is counted. The retention purge runs per source, strips raw from caddy_access rows older than raw_hours behind a watermark, and checks the cap through MAX(id)-MIN(id)+1 first, running COUNT(*) only when that bound exceeds the cap.
URL and query vocabulary (v1.3.42.1, endpoints aligned in v1.3.42.3): the panel keeps every page's filters in its URL with one set of names (range, host_id, ip, country, scenario, q, source, status, method, path, origin, scope, type, tab, page / offset, limit) and the endpoints take the same names: range (15m, 1h, 6h, 12h, 24h, 7d, 30d) on /api/logs, /api/logs/stats, /api/logs/timeseries, /api/logs/export.csv and /api/notifications/deliveries as the alternative to from/to (explicit from/to win), on /api/dashboard/* as before, and on /api/appsec/metrics as an alias of window; q on /api/threats/decisions as an alias of search. The Logs ip field is still the remote_ip substring filter.
Traffic chart data. Every /api/dashboard/* response carries generated_at plus X-Argos-Generated-At / X-Argos-Cache: hit\|stale\|stale-error\|miss headers (v1.3.38.2) and, on overview and traffic, X-Argos-Path: rollup\|rows (v1.3.42.3). 1h and 6h read rows (1 / 5 min buckets, exact percentiles, percentile_method: exact). 24h, 7d and 30d are stitched from the hourly rollup for the closed hours plus the rows of the two edges (percentile_method: histogram): the status series at 15 min (24h, index-only), 1 h (7d) or 6 h (30d); response_times one point per hour (6 h on 30d) from the rollup's duration histogram, each value the upper edge of the bucket holding the rank (50, 100, 250, 500, 1,000, 2,500 ms; 5,000 means 5,000 ms or more, the open bucket; the maximum is never reported, v1.3.42.3.1); top_hosts, top_paths and bandwidth_out_bytes over the whole range, exact. host_id applies on every range. The v1.3.38.4 fields detail_window, detail_from and series_covers_range are gone. While the rollup has no hours at all (a fresh install before its first fill) 7d and 30d answer the series and top hosts from the indexes with a note and empty response times, paths and bandwidth.
GET
/api/dashboard/security
Attack signal aggregates + map + top IPs. v1.3.39: both WAF engines. waf_engines = {coraza: {enabled_hosts, total_hosts, events}, appsec: {mode, hits, bans, events, blocked, logged}, events_total}; waf_timeseries[] buckets carry detected (Coraza audit rows), blocked (403 at the edge, any cause) and appsec (AppSec alerts); top_attack_types[] rows carry engine (coraza with rule_id, appsec with rule = scenario); top IPs, paths and the country map fold both engines. AppSec figures come from one cached LAPI fetch shared with /api/appsec/metrics and /api/security/overview. Limitation: an AppSec alert does not record whether the request was blocked; blocked / logged attribute each alert to the mode active when it fired.
The /api/security/* namespace (v1.3.24) maps 1:1 to the panel's /security tab strip. The older /api/threats/* paths still back the Threats page (see below).
Every active decision, geo-enriched, as a flat array (unchanged shape). Filters below apply.
GET
/api/threats/decisions?page=N&per_page=100
v1.3.38.5: paged envelope {decisions, total, page, per_page, pages}; page is 1-based and clamped to the last page, per_page 1-1000 (default 100). Only the page's rows are geo-enriched.
filters (both shapes)
origin and type exact (case-insensitive); search (or q, v1.3.42.3; value or scenario), ip (value), scenario as case-insensitive substrings; country two-letter code, matches Ip-scoped values by GeoIP only.
POST
/api/threats/decisions
Body {ip, duration_hours, reason}: manual ban (machine credentials required).
DELETE
/api/threats/decisions?ip=
Remove every active decision for the IP (the page's unban button).
Active decisions list. Filters: substring q (matches value or scenario), scope (Ip/Range/Country/AS), origin (e.g. cscli, argos-country-BR, CAPI). Cached for 15s.
DELETE
/api/security/decisions/{id}
Unban a single decision. Idempotent (404 from LAPI = already gone, returns success).
POST
/api/security/decisions/unban-ip
Body: {ip: "..."}. Removes every active decision for that IP.
GET
/api/security/whitelist
List manual whitelist entries. Returns {entries: [...]}.
Aggregated counters for the Banned IPs tab header.
GET
/api/security/check-self
SelfBlockBanner v2 data: panel's session IPs + public IP, cross-referenced against active bans.
GET
/api/security/public-ip-self
Cached panel public IP (refreshed hourly via ipify).
GET
/api/security/overview
Per-host security posture aggregates. v1.3.39: blocked_24h_total = Coraza rows + AppSec hits + AppSec bans, split in blocked_24h_by_engine {coraza, appsec}; appsec_mode, appsec_hits_24h, appsec_bans_24h, appsec_error (set when the LAPI could not be read; AppSec figures are then 0). Per host: engine (coraza / appsec / coraza+appsec / none), blocked_24h = coraza_24h + appsec_hits_24h (AppSec hits attributed to the host through the alert's target_fqdn; bans carry no host, so the per-host sum is appsec_bans_24h short of the total). alerts_critical_24h and waf_*_count stay Coraza-only.
GET
/api/crs/rules
Browse the CRS rule list for the WAF exclusions UI.
List installed scenarios. Returns {scenarios: [{short_name, source, canonical_name, path, disabled, description?}], is_available, mount_path, disabled_count, last_modified_at?}. The description field (v1.3.30) carries the hub-catalogue summary for the tooltip; empty when the slimmed argos-scenarios-index.json hasn't been emitted yet.
PATCH
/api/security/scenarios/{name}
Body: {disabled: bool}. Toggles the operator-disabled set. The name segment is URL-encoded (crowdsecurity%2FCVE-2017-9841); chi v5 captures the encoded form, the handler url.PathUnescapes before persisting. Writes argos-disabled-scenarios.txt sentinel.
The mark-applied endpoints (/api/security/scenarios/mark-applied, /api/security/appsec-tuning/mark-applied) shipped in v1.3.25 were retired in v1.3.27 along with the operator-trust model.
Country bans (v1.3.21+ + v1.3.31 async + v1.3.33 reconciler)¶
Method
Path
Purpose
POST
/api/security/countries/{cc}/expand
v1.3.31 path-based shape. Body: {duration: "168h", reason?: ""}. Returns 202 + the new job row (state=pending). Worker goroutine drives the LAPI POST chunk-by-chunk; poll /api/security/jobs/{id} for progress.
GET
/api/security/countries
List active expansions. Each row carries the v1.3.33 state field (active | drifted). v1.3.42.1: the cidrs arrays are omitted unless ?cidrs=1 (the panel renders cidr_count only; with them inline the list was 770 KB).
DELETE
/api/security/countries/{cc}
Synchronous revoke: DELETE /v1/decisions?origins=argos-country-XX to LAPI + DELETE FROM country_ban_expansions panel-side.
The pre-v1.3.31 body-based POST /api/security/countries/expand endpoint is removed.
List recent jobs. Empty country returns cross-country recent. limit defaults 20, max 200.
The /api/security/jobs path is intentionally top-level under /security/ (not nested under /countries) to leave room for future job types (audit retention sweeps, scenario re-installs, etc.) without further URL churn.
Rolling aggregates over a window (1h/6h/12h/24h). v1.3.39: total_hits = hits (alerts of kind=waf, one per request) + bans (the appsec-* scenarios of kind=crowdsec, bucket overflows with a decision); every window is served from one cached 24 h LAPI fetch (30 s) shared with the dashboard and the overview. The LAPI query is since=<minutes>m&include_capi=false&with_decisions=false&limit=5000 (the LAPI default is 100 and there is no pagination; since/until are relative durations only). blocked / logged attribution by mode boundary, as above.