HTTP API reference

Every endpoint returns JSON unless noted. Timestamps named ts are Unix milliseconds. The dashboard UI is a static page that only uses this public API — anything the UI can do, you can script.

No SDK, no client library. Plain curl is enough. All examples below use -u admin:$AUTH_PASS for Basic auth and assume http://localhost:8080.

Authentication

Every endpoint except /healthz requires HTTP Basic auth when AUTH_PASS is set (the default posture). Credentials are compared in constant time.

bash
curl -s -u admin:'change-me' http://localhost:8080/api/apps

# or with a header
curl -s -H 'Authorization: Basic YWRtaW46Y2hhbmdlLW1l' http://localhost:8080/api/containers

Health

GET /healthz no auth · use for probes

Returns the plain text ok. The image carries a matching Docker HEALTHCHECK, so use the same path on your platform's HTTP health check.

Containers and apps

GET /api/containers currently followed containers
200 OK
{"containers":[{"id":"...","name":"srv-captain--shop.1.x","app":"shop","replica":"1",
  "image":"shop:1.4","started":1793158700000,"cpu":12.5,"mem":104857600,
  "mem_limit":536870912,"sample_ts":1793158700000}]}
GET /api/apps apps known from history

Includes apps whose containers are gone — that is the point. Each row carries a one-hour error and warning count.

200 OK
{"apps":[{"app":"shop","containers":1,"last_seen":1793158700437,
  "errors_1h":8,"warns_1h":14}]}

Logs

GET /api/logs rows oldest first
ParamMeaning
appApp name.
containerContainer name or id prefix.
qSearch text, case-insensitive. Uses the FTS5 token/prefix index as a fast path (each word matches as a prefix, so dat finds database); when the index finds nothing it falls back to LIKE, so mid-word searches still work.
regex1 treats q as a case-insensitive regular expression. Invalid patterns return 400.
levelComma list of error, warn, info, debug.
streamstdout or stderr.
sinceRelative (15m, 2h, 1d) or epoch milliseconds.
untilEpoch milliseconds.
after_idOnly rows with a larger id, ascending — use for catching up.
before_idOnly rows with a smaller id — use for paging back.
limitDefault 500, max 5000. Without after_id you get the newest limit rows.
row shape
{"id":260,"ts":1793158659437,"container_id":"...","container":"srv-captain--shop.1.x",
 "app":"shop","stream":"o","level":"error","line":"ERROR ..."}

stream is o for stdout and e for stderr. level may be empty when the heuristic finds no level word.

GET /api/logs/stream Server-Sent Events

A Server-Sent Events stream of new rows matching the same filters (except paging and time params). Each data: line is one row. A dropped event reports how many rows this client lost to back-pressure. Comment lines (: ping) keep the connection alive.

bash — tail one app
curl -N -u admin:$AUTH_PASS 'http://localhost:8080/api/logs/stream?app=shop&level=error'
GET /api/logs/export CSV · up to 200,000 lines

Streams matching rows as capscope-logs.csv, honoring the same filters as /api/logs (app, q, level, since, …). It paginates internally, so exports reach up to 200,000 lines regardless of the normal 5000-row cap.

Access logs (GoAccess-style)

Parsed web-server access-log rows — nginx combined/common, CapRover's captain-nginx defaults and most proxy formats — are collected automatically:

  • Containers whose app name looks like a reverse proxy are parsed from the first line.
  • Any other container is probed and switched on once most of its recent stdout parses as access logs.
  • Containers matched by EXCLUDE are followed only for this purpose: no rows in logs, no sidebar entry, but their requests still feed the dashboard.
  • ACCESS_LOG selects auto (default), all or off; ANON_IPS=false stores original IPs (default anonymizes: last octet zeroed, IPv6 cut to /48).
  • Retention follows LOG_RETENTION.
GET /api/access aggregated dashboard data
ParamDefaultMeaning
range1hWindow such as 1h, 24h, 7d.
appallRestrict to one app.
top20Rows returned per path/IP/referrer list.
200 OK
{"since_ms":...,"until_ms":0,"bucket_ms":12000,
 "totals":{"requests":900,"client_err":41,"server_err":9,"unique_ips":3,"bytes":88401},
 "codes":[{"code":200,"n":600}],
 "paths":[{"key":"/api/items","hits":212,"bytes":9901,"errors":3}],
 "ips":[{"key":"203.0.113.0","hits":400,"bytes":41000,"errors":2}],
 "refs":[{"key":"https://google.com/","hits":12,"errors":0}],
 "timeline":[{"ts":1793157492000,"hits":30,"errors":1,"bytes":2955}]}

refs excludes empty and - referrers.

GET /api/access/export CSV · 100,000 rows

Streams raw parsed rows as capscope-access.csv: ts, ts_ms, container, app, ip, method, path, status, bytes, referer, user_agent. Default range 24 h.

SQL queries

GET /api/sql read-only ad-hoc query
ParamMeaning
queryA single SELECT (or WITH … SELECT) statement.
limitDefault 1000, max 5000. truncated is true when more rows matched.
200 OK
{"columns":["level","n"],"rows":[["error",3],["info",41]],"truncated":false,"ms":1}

Rules, enforced in order

  1. The statement must start with SELECT or WITH, contain no stacked statements (;), and carry no write/DDL keywords outside string literals and comments. Violations return 400.
  2. The read connection pool opens with _query_only=1, so any write that slipped through still fails at SQLite level.
  3. A 5-second deadline and the row cap keep heavy scans from pinning the pool.

Tables: logs, metrics, containers, snapshots (plus logs_fts for MATCH joins). Column notes: logs.ts is unix nanoseconds; metrics.ts and snapshots.ts are unix milliseconds; snapshots.die_ts is the death time in milliseconds (0 for rows saved before v0.5); logs.stream is o/e.

sql
SELECT app, level, count(*) AS n FROM logs
WHERE ts > (unixepoch('now') - 3600) * 1000000000
GROUP BY app, level HAVING n > 5 ORDER BY n DESC

The UI exposes this as the SQL option in the search-mode select.

Metrics

GET /api/metrics bucketed points
ParamMeaning
appApp name — CPU and memory are summed across replicas.
rangeWindow such as 15m, 1h, 7d.
containerContainer id prefix, for a single replica.

Expect about 300 points per range with a minimum bucket of 5 s. net_rx/net_tx are cumulative counters.

200 OK
{"bucket_ms":12000,"points":[{"ts":1793157492000,"cpu":10,"mem":104857600,
  "mem_limit":536870912,"net_rx":0,"net_tx":0,"blk_r":0,"blk_w":0,"pids":0}]}

Crash snapshots

GET /api/snapshots light rows · newest first

?app=shop&limit=100 — metadata only, no embedded logs or metrics.

GET /api/snapshots/{id} full snapshot

Metadata, summary, logs, metrics and a trimmed inspect object — plus the newest deploy before the crash as a deploy object when one exists.

GET /api/snapshots/{id}/export Markdown incident report

A Markdown crash report suitable for pasting into an incident channel, including exit code, restart window, last deploy and the log tail.

FieldMeaning
kindOne of oom, crash, killed, clean_exit.
die_tsDeath time taken from the Docker event — a snapshot rebuilt after CapScope was down keeps the real death time, not the assembly time.
restarts_windowHow many times that container had died inside the restart-loop window at that moment (0 when detection is disabled). The UI marks rows at or above the threshold with ↻.

Deaths that happened while CapScope was down are replayed from the persisted event cursor on startup and snapshotted like live ones, as long as the container still exists and the gap is under 24 hours.

Alerts

Rules from ALERTS_FILE are evaluated every ALERTS_INTERVAL (default 1m). Every firing is recorded, and the most recent firing per rule and app doubles as the silence window that prevents re-notifying on every tick — a repeat on the rule (default 30m) sets that silence.

GET /api/alerts firing history

?app=shop&limit=100 — newest first. Each row is {id, ts, rule, app, kind, message}, where ts is Unix ms and kind is the rule kind.

GET /api/alerts/rules armed configuration · no secrets
200 OK
{"rules":[{"name":"api-mem","kind":"metric","apps":["api"],"for":"5m",
  "metric":"mem_pct","op":"gt","value":85,"repeat":"30m"}],
 "channels":["webhook"],"enabled":true}

enabled is false when no rules file exists.

POST /api/alerts/test verify channels

Sends a test message through every configured channel and returns {results: {webhook: "sent" | "failed: …"}}. Returns 400 when no channels are configured or alerting is off.

bash
curl -s -X POST -u admin:$AUTH_PASS http://localhost:8080/api/alerts/test

Deploys

Deploy markers correlate a crash or a metric spike with the release that preceded it. Record them from CI (POST) or with the Record deploy button in the Metrics tab.

POST /api/deploys record a release marker

Body {app, version, note?, ts?}. app is required; ts is optional Unix ms and defaults to now (a far-future value is clamped to now). Returns {id, ts, app, version, note}.

bash — CI step
curl -s -X POST -u admin:$AUTH_PASS \
  -H 'Content-Type: application/json' \
  -d '{"app":"shop","version":"v1.4.2","note":"rollout"}' \
  http://localhost:8080/api/deploys
GET /api/deploys markers · newest first

?app=shop&since=1700000000000&until=&limit=200 — app, since and until (Unix ms) are optional; an empty app returns every app's markers.

Markers appear as dashed vertical lines with a version label on the CPU and memory charts, and as inline rows in the log timeline. The crash detail and its Markdown export carry the newest marker before the death as Last deploy.

Recipe — find every crash that followed a release:
bash
curl -s -G -u admin:$AUTH_PASS http://localhost:8080/api/sql \
  --data-urlencode "query=SELECT s.die_ts, s.app, s.kind, d.version FROM snapshots s LEFT JOIN deploys d ON d.app = s.app AND d.ts = s.die_ts ORDER BY s.die_ts DESC LIMIT 50"