API schema and limits¶
KubeManta's agent is a REST API, and everything the dashboard does it does through that API. Anything you can do in the UI you can script — subject to the limits below, which exist so that a leaked key cannot become a cluster takeover.
The schema is served by the product¶
The agent publishes its own OpenAPI schema, so the authoritative endpoint list is the one running in your cluster rather than a page that can drift from it:
| Path | What it is |
|---|---|
/api/agent/openapi.json |
OpenAPI 3 schema — every route, parameter and response model |
/api/agent/docs |
Swagger UI — browse and try requests interactively |
Both are OFF by default and return 404 until an administrator opts in, by
setting KUBEMANTA_ENABLE_API_DOCS=true on the agent. That is deliberate: a
schema endpoint reachable without being asked for hands anyone who reaches your
agent a complete map of it, and most installs never need one.
With it enabled, both require a signed-in browser session — no API key can read
them. In a browser you are already signed in, so https://<your-host>/api/agent/docs
opens directly; download https://<your-host>/api/agent/openapi.json from the same
session to generate a client.
Turn it back off when you are done generating a client — it is a debugging and integration aid, not something to leave on.
Generate a client
The schema is ordinary OpenAPI 3, so openapi-generator, oapi-codegen and
friends will produce a typed client from it directly.
Authentication¶
Two credential kinds reach the API, and they are not equivalent.
Session cookie (km_session) — what the dashboard uses. HMAC-signed, sliding idle
expiry, absolute 7-day ceiling. Not intended for scripts.
Per-user API key (X-API-Key) — what automation uses.
Keys look like km_<prefix>_<secret>. Only a SHA-256 hash is stored, so the secret is
shown once at creation and cannot be recovered — reissue rather than hunt for it.
Creating a key¶
- Each user creates their own keys from the account menu → API keys, in a signed-in session. Admins can list and revoke any user's keys in Admin → Users & Groups → Users, but cannot create a key for someone else.
A key belongs to a user, and that is the point: it carries its owner's role, and the role is re-read from the database on every request. Demote the owner to viewer and the key immediately acts as a viewer; deactivate them, revoke the key, or let it expire and the next call gets a 401. There is no shared secret and nothing to redeploy.
The old shared key is gone
security.apiKey was removed. It was unattributable — every caller looked like the
same root principal — could not be revoked without a redeploy, and bypassed the seat
model. If you are following an older guide that sets it, that value does nothing.
Attribution¶
Key requests are audited as <username>@<ip>, so a shared key is visible as one account
calling from many addresses. That is deliberate: it makes credential sharing a thing you
can see in Activity rather than a thing you have to trust people about.
Scopes — what a key may do¶
Every key carries scopes, chosen when you mint it. A key can reach only the
surfaces its scopes name; everything else answers
403 {"error": "api_key_insufficient_scope"} and tells you which scope was needed.
Nothing is pre-selected in the picker. Grant only what the integration needs — a leaked key can do exactly that much and no more.
| Scope | Grants |
|---|---|
activity:read |
Read the activity feed |
activity:write |
Change activity retention |
ai:invoke |
Run AI diagnosis, chat and reports |
alerts:read |
Read alert rules, history and integrations |
alerts:write |
Create, modify and delete alert rules and integrations |
apps:read |
Read application stacks |
apps:write |
Create and modify application stacks |
builder:read |
Read saved Resource Builder bundles |
builder:write |
Plan and apply Resource Builder manifests |
helm:read |
Read Helm releases and values |
helm:write |
Install, upgrade and uninstall Helm releases |
mcp:read |
Use the read-only MCP tool surface |
metrics:read |
Read metrics, trends, capacity and observability config (not GET /metrics, which needs scrape:read) |
metrics:write |
Change the metrics source and deploy or remove Prometheus |
policy:read |
Read AI guardrails, access-control policy and seat counts |
scanning:read |
Read vulnerability reports and posture |
scrape:read |
Scrape KubeManta's own Prometheus metrics (GET /metrics) and nothing else. The one scope whose key is also accepted as Authorization: Bearer, the way Prometheus sends it — see Observability |
scanning:write |
Trigger scans and accept risk |
topology:read |
Read the cluster graph, network reachability and cloud topology |
topology:write |
Save cluster graph layouts |
workloads:read |
Read pods, deployments, namespaces, nodes, events and logs |
workloads:write |
Restart, scale, delete and cordon workloads |
workspace:read |
Read the Log Workspace |
workspace:write |
Import and delete Log Workspace content |
Two limits apply together, and both only narrow:
- Scopes decide which surfaces the key may reach.
- The owner's role, re-read on every request. Demote the owner and the key acts as a viewer whatever its scopes say; deactivate them and it stops working. A key can never exceed the person who holds it.
What no key can do, at any scope¶
Some surfaces map to no scope at all, so no combination reaches them. They answer
403 {"error": "api_key_forbidden"}.
- Identity and credentials — users, groups, SSO, OAuth clients, other API keys, your password, your second factor. A credential must never manage the credentials that govern it.
- Escalation and safety controls — expert mode, the AI kill switch, redaction
settings, IP bans. Reading policy is allowed under
policy:readso automation can report on it; changing it needs a signed-in session. - Anything that decrypts on read — saved Helm configs and Builder bundles can hold credentials, so reading one is exfiltration rather than inspection.
- The audit feed, which merges terminal command history and AI prompt text.
- The terminal and pod exec, refused by credential kind as well as by scope, so a WebSocket route added later cannot inherit key access.
The reasoning is one scenario: a leaked key should not be able to enable expert mode, apply a manifest as cluster-admin, disable redaction, ban the operators, and then purge the audit trail — each step is individually plausible, and together they are a takeover that needs no browser and no shell.
A route that maps to no scope is unreachable by construction rather than by a maintained list, so a surface added tomorrow is refused the day it lands.
Base path¶
Through the UI (the normal case), every agent route is under /api/agent:
Talking to the agent Service directly inside the cluster, drop that prefix:
Setting up¶
Every example below assumes these two variables:
Every call on this page is a read, viewer-safe and available on Free unless a line says otherwise, so a dashboard or a nightly report needs nothing more than a viewer's key. A key carries its owner's role: a viewer's key can do what a viewer can do, which is more than reading (acknowledging an incident, for example), and never more.
Inventory¶
GET /namespaces — the namespaces this credential can see, after the
allowlist and the system-namespace filter.
GET /pods/{namespace} — pods in one namespace.
curl -s "$KM/namespaces" -H "X-API-Key: $KM_API_KEY" | jq -r '.namespaces[]'
curl -s "$KM/pods/prod" -H "X-API-Key: $KM_API_KEY" | jq '.pods[].name'
GET /resources/describe/{group}/{version}/{plural}/{name} — what
kubectl describe prints for one object, as text, with the kind's own layout
where kubectl has one and kubectl's generic layout otherwise (CRDs). Pass
?namespace= for a namespaced kind and core as the group for the core API
group. Payload values in a data or stringData map show as their size, and
Secret is refused with a 400. Needs workloads:read.
curl -s "$KM/resources/describe/apps/v1/deployments/web?namespace=prod" \
-H "X-API-Key: $KM_API_KEY" | jq -r .text
Namespace health — GET /overview¶
The Overview screen in one call: a health verdict per namespace, worst first. No parameters. Served from a short server-side cache, and the response says so, so a polling dashboard does not need to guess a safe interval.
curl -s "$KM/overview" -H "X-API-Key: $KM_API_KEY" \
| jq '.namespaces[] | select(.health_score != "healthy")
| {namespace, health_score, pods_running, pods_total, deployments_degraded}'
{
"namespaces": [
{
"namespace": "prod",
"health_score": "degraded",
"pods_running": 24,
"pods_failing": 1,
"pods_pending": 0,
"pods_total": 25,
"pods_capped": false,
"active_alerts": 2,
"deployments_total": 9,
"deployments_degraded": 1,
"services_total": 11,
"daemonsets_total": 2,
"pvcs_unbound": 0,
"last_event_time": "2026-08-19T09:41:02+00:00",
"summary": {
"pods": { "running": 24, "total": 25 },
"deployments": 9,
"services": 11,
"daemonsets": 2
}
}
],
"total_namespaces": 1,
"cached_at": "2026-08-19T09:42:10.113402+00:00",
"cache_ttl_secs": 30
}
health_score is one of critical, degraded, unknown or healthy, and the
list arrives sorted in exactly that order — the first entry is the one to look
at. A namespace whose health could not be computed comes back as unknown
rather than being dropped, and sorts above healthy, so a failure to read is
never mistaken for a clean bill.
Incidents¶
GET /incidents — one entry per pod that is unhealthy, crashing, stuck
pending, or close to its memory limit. Optional namespace= (omit, or *, for
cluster-wide). Computed from a single pod LIST, so it is cheap to poll.
curl -s "$KM/incidents?namespace=prod" -H "X-API-Key: $KM_API_KEY" \
| jq '{total, critical, warning}'
{
"incidents": [
{
"id": "prod/checkout-7d9f6-2xk4p/CrashLoopBackOff",
"namespace": "prod",
"pod": "checkout-7d9f6-2xk4p",
"workload_kind": "Deployment",
"workload_name": "checkout",
"reason": "CrashLoopBackOff",
"detail": "back-off 5m0s restarting failed container",
"severity": "critical",
"phase": "Running",
"container": "checkout",
"restart_count": 14,
"restart_rate": 3.2,
"first_failure_ts": "2026-08-19T08:55:11+00:00",
"crashing_for_secs": 2820,
"last_exit_code": 1,
"age": "3h"
}
],
"total": 1,
"critical": 1,
"warning": 0,
"metrics_available": true,
"generated_at": "2026-08-19T09:42:31.004918+00:00"
}
metrics_available is false when metrics-server is absent. A memory-pressure
incident (reason: "OOMRisk") additionally carries mem_pct_of_limit.
GET /incidents/{namespace}/{pod} — the whole story for one pod: its
incident record, the recent event timeline, the last exit, a redacted log tail,
and live usage against requests and limits.
curl -s "$KM/incidents/prod/checkout-7d9f6-2xk4p" -H "X-API-Key: $KM_API_KEY" \
| jq '{exit: .last_exit, cpu: .resources.cpu_pct_of_limit,
mem: .resources.mem_pct_of_limit, events: (.events | length)}'
| Key | What it holds |
|---|---|
incident |
The same record shape as the list above |
last_exit |
container, exit_code, reason, signal, started_at, finished_at — or null if nothing has terminated |
events |
Up to ~15 recent Kubernetes events: type, reason, message, count, last_seen, age |
log_tail |
~40 redacted log lines as a single string. For a restarted container this is the previous instance — the run that crashed |
resources |
available, cpu_usage_m, cpu_request_m, cpu_limit_m, cpu_pct_of_limit, mem_usage_bytes, mem_request_bytes, mem_limit_bytes, mem_pct_of_limit |
generated_at |
ISO-8601 timestamp |
Secrets are stripped before they reach you
log_tail and event messages are redacted server-side by the same rules that
protect AI prompts. A token that scrolled past in a log does not leave the
cluster through this endpoint.
Alerts — GET /alerts¶
GET /alerts (alerts:read) — what is firing now (active) and the alert
history (history): one entry per firing — from your rules, the built-in
alerts and Alertmanager — newest first. The history is stored in KubeManta's
database, so an agent restart does not shorten it, and it is kept for the
alert-history retention window (180 days by default).
| Parameter | Default | Meaning |
|---|---|---|
limit |
500 |
How many history entries to return, 1–5000. |
active_only |
false |
true returns no history. |
namespace |
all | Narrow both lists to one namespace; active_elsewhere then lists what is firing in the others. |
Clearing the history — from the Alerts tab, DELETE /alerts/history or
DELETE /alerts/firings (admin, alerts:write) — clears that one list
everywhere it appears, including Activity and incident reports.
Upgrading from 1.3.1 or earlier
Up to 1.3.1, history was an in-memory list of at most 500 entries that a
restart emptied, and DELETE /alerts/history did not clear the list the
Alerts tab shows. Earlier history is folded into the database on the first
start after the upgrade.
Resource history — GET /metrics/history¶
CPU and memory over time, plus network rates where they are collected. This is what the Resource Trends charts draw, and it is the endpoint to build a capacity dashboard on.
| Parameter | Values | Notes |
|---|---|---|
scope |
cluster (default), namespace, pod |
|
namespace |
a namespace name | Required for scope=namespace and scope=pod |
name |
a pod name | Required for scope=pod |
window |
1h, 6h, 24h, 7d |
Default 1h |
Anything else is a 400 naming the field. The window is bucketed server-side to
roughly 120 points, so a 7-day query is still a small response.
curl -s "$KM/metrics/history?scope=namespace&namespace=prod&window=24h" \
-H "X-API-Key: $KM_API_KEY" | jq '.bucket_secs, (.series | length), .series[-1]'
{
"available": true,
"scope": "namespace",
"namespace": "prod",
"name": null,
"window": "24h",
"bucket_secs": 720,
"sample_interval_secs": 60,
"retention_days": 7,
"network": {
"enabled": true,
"available": true,
"last_scrape_ok": true,
"last_scrape_error": null
},
"series": [
{
"ts": 1755594000,
"cpu_millicores": 1420,
"mem_bytes": 3221225472,
"net_rx_bps": 18422,
"net_tx_bps": 9310
}
]
}
The points live in series, and each one is ts (bucket start, epoch
seconds), cpu_millicores, mem_bytes, and the two network rates.
Network is opt-in. net_rx_bps and net_tx_bps are null unless the
kubelet scrape is enabled (metrics.kubeletScrape.enabled in your Helm values),
and network.enabled tells you which install you are on. Rates are derived from
raw counters at read time, and an interval spanning a counter reset — a pod
restart — is skipped rather than reported as a spike.
retention_days is how far back the samples go; ask for a wider window than
that and the earlier part simply has no points. For longer history, wire up
Prometheus.
Kubernetes API server — GET /metrics/apiserver¶
Request rate, error rate, p99 latency and a breakdown by verb for the cluster's own API server. No parameters.
This one requires Prometheus. With none configured you get
{"source": "none", "setup_required": true} and nothing else — check that before
reading any other field.
{
"source": "prometheus",
"requests_per_sec": 184.31,
"error_rate_pct": 0.04,
"p99_latency_ms": 212.4,
"by_verb": [
{ "verb": "GET", "rate": 91.204 },
{ "verb": "WATCH", "rate": 44.118 },
{ "verb": "LIST", "rate": 20.771 }
]
}
Each field is fetched independently, so one query failing drops that field
rather than the whole response — branch on presence, not on a status code. On a
managed control plane that publishes no apiserver_* series, source stays
"prometheus" and the metrics are simply absent.
by_verb is the top verbs by rate. The latency and error-rate figures exclude
WATCH and CONNECT: those are long-poll verbs whose durations are minutes by
nature, and including them pins p99 to the histogram's top bucket.
Applications¶
An application is a saved label selector across one or more namespaces. These
two endpoints answer is it the right size and is its traffic moving, for
whatever {id} you get back from GET /apps.
GET /apps/{id}/metrics¶
Usage against requests and limits, per member pod and in total, plus a current traffic reading per workload.
curl -s "$KM/apps/$APP_ID/metrics" -H "X-API-Key: $KM_API_KEY" \
| jq '{metrics_available, totals, flags}'
{
"id": "a2f1…", "name": "checkout", "namespaces": ["prod"],
"metrics_available": true,
"reason": null,
"totals": {
"cpu_millicores_used": 840,
"cpu_millicores_request": 1200,
"cpu_millicores_limit": 2000,
"mem_bytes_used": 2415919104,
"mem_bytes_request": 3221225472,
"mem_bytes_limit": 4294967296,
"pods_measured": 6,
"pods_total": 6
},
"members": [
{
"namespace": "prod", "pod": "checkout-7d9f6-2xk4p", "workload": "checkout",
"cpu_millicores_used": 140, "mem_bytes_used": 402653184,
"cpu_millicores_request": 200, "cpu_millicores_limit": 400,
"mem_bytes_request": 536870912, "mem_bytes_limit": 715827882,
"flags": ["memory_above_request"]
}
],
"flags": ["memory_above_request"],
"network": {
"available": true,
"source": "prometheus",
"workloads": [
{ "workload": "checkout", "rx_bytes_per_sec": 14203.5, "tx_bytes_per_sec": 8811.2 }
]
}
}
Three things worth knowing before you build on it:
- Absence is never zero.
cpu_millicores_usedandmem_bytes_usedarenull— not0— for a pod metrics-server could not measure, andmetrics_available: falsecarries thereason. A zero would render an unmonitored application as an idle one. totalscovers the measured pods only, which is whatpods_measuredandpods_totalare for. Summing requests over every pod while usage covered a subset would inflate the request side and make an over-subscribed application read as comfortably provisioned.- Network is per workload and never summed, because one total hides "the
database is saturated while the web tier is idle".
sourceisprometheusorkubelet; when neither can answer,availableisfalseandreasonsays what to enable.
Per-member flags name a state rather than leaving you to interpret a ratio:
no_requests, no_cpu_request, no_memory_request, near_memory_limit,
cpu_above_request, memory_above_request.
GET /apps/{id}/network-history¶
The same traffic as a series. Takes window — 1h (default), 6h, 24h or
7d, the same set as /metrics/history.
curl -s "$KM/apps/$APP_ID/network-history?window=6h" -H "X-API-Key: $KM_API_KEY" \
| jq '.workloads[] | {workload, points: (.series | length)}'
{
"available": true,
"source": "prometheus",
"window": "6h",
"bucket_secs": 180,
"workloads": [
{
"workload": "checkout",
"series": [
{ "ts": 1755594000, "rx_bps": 14203.5, "tx_bps": 8811.2 }
]
}
]
}
Points are ts (epoch seconds), rx_bps, tx_bps. When the series comes from
our own kubelet samples rather than Prometheus, source is "kubelet" and a
sample_interval_secs field comes with it — the same rate at two resolutions
means different things, so the response always names which one answered. With
neither source available, available is false and reason says why.
Security posture and findings¶
# Current posture — grade, severity counts, coverage
curl -s "$KM/security/posture?namespace=prod" -H "X-API-Key: $KM_API_KEY" \
| jq '{grade, score, by_severity, workloads_covered}'
# Findings, worst first, paginated
curl -s "$KM/security/findings?page=1&page_size=50&sort=severity" \
-H "X-API-Key: $KM_API_KEY" | jq '.total, .rows[0]'
GET /security/findings filters with namespace, severity, kind
(vuln or misconfig), fixable=true and search. Sorting is server-side —
sort is one of severity (default), cvss, fixable, namespace,
workload or affected — and so is paging: pass page and page_size and
the response adds rows, page, page_size, total, has_more and sort
alongside the usual vulns and misconfigs. Omit page and you get the
complete document, streamed.
group_by=cve returns one row per CVE, the view the product opens on, with how
far it reaches: workloads, images and sightings. sort=affected puts the CVEs
that reach the most workloads first. cve=<id> returns one CVE's sightings, a
page at a time, under the same filters. group_by=image returns one row per
image and CVE.
Each vulnerability carries cvss (the CVSS v3.x base score) with
cvss_version (3.0, 3.1, or empty when the report gave no vector),
cvss_source (nvd, redhat, ghsa or another vendor a trivy-operator report names; empty when the report does not say)
and cvss_vector. A finding stored before 1.5.0 gains them when its image is
next scanned.
# The CVEs that reach the most workloads in one namespace
curl -s "$KM/security/findings?namespace=prod&group_by=cve&sort=affected&page=1&page_size=20" \
-H "X-API-Key: $KM_API_KEY" | jq '.rows[0]'
An out-of-range page clamps to the last page instead of erroring, so narrowing
a filter never throws away the filter you just built.
GET /security/workload-badges/{namespace}¶
The vulnerable-image summary behind the Workloads CVSS column: one entry per
workload, keyed Kind/name, for every workload with a current, non-excluded
high or critical finding from a finished scan. A workload that is absent has no
such finding or has not been scanned; there is no "clean" entry. The worst CVE
carries cvss_version, cvss_source and cvss_vector, each null when the
report did not say.
GET /security/findings/export¶
The same filters, rendered as a file and streamed, so a large export starts at
once. format is json (default), csv, md or html, and group_by=cve
works in every format.
# CSV of the fixable criticals in one namespace
curl -s "$KM/security/findings/export?format=csv&namespace=prod&severity=CRITICAL&fixable=true" \
-H "X-API-Key: $KM_API_KEY" > findings.csv
# Everything, as JSON
curl -s "$KM/security/findings/export?format=json" -H "X-API-Key: $KM_API_KEY" \
> findings.json
CSV is single-type, because two record shapes do not share a header row: kind
selects which, and defaults to vuln. Vulnerability columns are namespace,
workload, image, cve, severity, cvss, cvss_version, cvss_source,
pkg, installed_ver, fixed_ver, fix_available, source; with
group_by=cve they are cve, severity, cvss, cvss_version,
cvss_source, fix_available, fixed_versions, fixable_images, images,
workloads, sightings and packages; misconfiguration columns are
namespace, workload, check_id, severity, message, resource,
source. json returns the same {vulns, misconfigs} payload the findings
endpoint serves, and md and html render both tables as a document you can
attach to a ticket.
Rate limits and errors¶
| Status | Meaning |
|---|---|
401 |
No credential, an expired session, or a revoked/expired key |
402 |
The endpoint needs a license tier you do not have — the body carries feature and an upgrade hint |
403 |
Authenticated but not permitted: a viewer calling a write, a denied IP, or api_key_forbidden |
429 |
Login lockout, or the discovery rate limit |
503 |
The agent is shedding load (in-flight cap) — retry with the Retry-After header |
Errors are JSON objects with an error field where the distinction matters
programmatically, so branch on that rather than parsing prose.
See also¶
- MCP server — the read-only tool surface for AI agents, same key
- Security model — why keys are per-user
- Access control — IP allow/deny and auto-ban