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 still require authentication like every other route. In a
browser you are already signed in, so https://<your-host>/api/agent/docs opens
directly. From a script:
curl -s https://kubemanta.example.com/api/agent/openapi.json \
-H "X-API-Key: $KM_API_KEY" | jq '.paths | keys[]'
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¶
- For yourself: the account menu → API keys, or
POST /me/api-keys. - For another user (admin): Admin → Users → pick the user → API keys → Create.
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 |
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 |
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:
Everything here is read-only, 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.
Inventory¶
GET /namespaces — the namespaces this credential can see, after the
allowlist and the system-namespace filter.
GET /namespaces/{namespace}/pods — pods in one namespace.
curl -s "$KM/namespaces" -H "X-API-Key: $KM_API_KEY" | jq '.namespaces[].name'
curl -s "$KM/namespaces/prod/pods" -H "X-API-Key: $KM_API_KEY" | jq '.pods[].name'
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.
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, group_by=image and search. Sorting
is server-side — sort is one of severity (default), cvss, fixable,
namespace or workload — 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 everything.
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/findings/export¶
The same filters, rendered as a file. format is json (default), csv, md
or html.
# 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, pkg, installed_ver,
fixed_ver, fix_available, source; 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