Skip to content

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.

curl -s https://kubemanta.example.com/api/agent/namespaces \
  -H "X-API-Key: km_a1b2c3d4_…"

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:read so 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:

https://<your-host>/api/agent/<route>

Talking to the agent Service directly inside the cluster, drop that prefix:

http://kubemanta-agent.kubemanta-system.svc:8080/<route>

Setting up

Every example below assumes these two variables:

export KM=https://kubemanta.example.com/api/agent
export KM_API_KEY=km_a1b2c3d4_…

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.

curl -s "$KM/metrics/apiserver" -H "X-API-Key: $KM_API_KEY"
{
  "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_used and mem_bytes_used are null — not 0 — for a pod metrics-server could not measure, and metrics_available: false carries the reason. A zero would render an unmonitored application as an idle one.
  • totals covers the measured pods only, which is what pods_measured and pods_total are 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". source is prometheus or kubelet; when neither can answer, available is false and reason says 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 window1h (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