Reference
HTTP API
The JSON API, the endpoints for agents, browsers, and CI, and authentication.
The panel uses the JSON API at /api/v1. Response shapes may change between minor
versions. There is no OpenAPI document.
Paths
| Method and path | What it is | Auth |
|---|---|---|
GET / and other panel paths | the panel | none to load the panel |
GET /assets/* | static files of the panel, cached for a year | none |
/api/v1/... | the JSON API, see below | session or token |
POST /api/deploys | mark a deploy, see Deploy API | token (deploy) |
POST /api/changes | mark a change such as a migration, see Change API | token (deploy) |
POST /hooks/github | the GitHub webhook, see GitHub | HMAC signature |
GET, POST /ping/<token> | heartbeats, plus /start, /fail, and /<exit code 0-255> | the ping token in the URL |
GET, POST /api/push/<token> | heartbeats as push URLs, with ?status=up or another status for a failure; answers JSON {"ok":true}, see Keep an existing push URL | the ping token in the URL |
GET /b.js (also /script.js), POST /e | the tracking script and its events | none, the site must be known |
POST /agent/v1/frames, GET /agent/v1/poll | agents | host token |
POST /mcp | the MCP server | token (mcp) |
GET /healthz | liveness: ok and 200 when healthy, the reason and 503 when not | none |
GET /metrics | Prometheus metrics about kogut itself | session or token (read) |
GET /status | the status page on the panel’s own address | none |
POST /status/subscribe | subscribes the form’s email to email updates and sends a confirmation link on both hosts | none |
GET/POST /status/confirm?token=... | a page with a button, and the confirmation it posts | the token |
GET/POST /status/unsubscribe?token=... | a page with a button, and the unsubscribe it posts, which also serves as the one-click unsubscribe of mail apps | the token |
| any other path on the status domain | the status page, only GET and HEAD | none |
Unknown paths under /api, /agent, /auth, /e, /ping, /hooks, and /mcp answer
404. Other unknown paths load the panel.
/api/v1
Reads take range=1h, 24h, 7d, 30d, or 1y, or from and to in milliseconds
(up to 400 days), but not both.
| Method and path | What it does | Who |
|---|---|---|
GET /api/v1/auth | {needsSetup, user, password, providers, proxy}: whether the first account is missing, the signed-in user, if any, whether passwords are on, {name, url} of each OpenID Connect provider, and whether a proxy signs people in | anyone |
POST /api/v1/auth/setup | create the first account with the setup token: {token, email, password} | anyone with the setup token |
POST /api/v1/auth/login | sign in: {email, password}. Sets the session cookie | anyone |
POST /api/v1/auth/logout | sign out | session |
GET /api/v1/auth/me | the signed-in user: {email, role} | session |
GET /api/v1/config/status | the current kogut.toml error, or null | read |
GET /api/v1/config/export | {toml, secrets}: what was created in the panel as kogut.toml, the text kogut export prints, and the {name, key, about} of each ${NAME} it needs, without values | write |
GET /api/v1/overview | the overview screen, default range 24h | read |
GET /api/v1/warnings | what could keep alerts from arriving: a host that shares kogut’s machine while neither kogut edge nor a heartbeat URL is set (sharedHost), the number of notification channels (channels), and a private proxy that is not trusted while nearly all recent page views come through it (untrustedProxy: address and sharePercent, or null) | read |
GET /api/v1/services, /services/{name} | services, and one service | read |
GET /api/v1/incidents, /incidents/{id} | open incidents and those resolved in the range (default 30d, at most 200), and one incident | read |
POST /api/v1/incidents/{id}/ack | acknowledge an incident, which stops reminders | write |
GET /api/v1/hosts, /hosts/{name} | hosts, and one host. A host’s checkConflicts lists the command checks in its agent’s file that were not added: {name, with, host}, where with is file, panel, or host for the check that has the name, or free when the check is added at the agent’s next declaration, within an hour | read |
GET /api/v1/hosts/{name}/containers/{container}, /hosts/{name}/units/{unit} | CPU (percent of one CPU) and memory (bytes) of one container or systemd unit over the range, default 24h: {x, stepMs, cpu, cpuMax, mem, memMax}, with null where it did not run | read |
POST /api/v1/hosts | add a host, or replace its token: {name}. Returns the host token bth_... and the install command once | write |
PUT /api/v1/hosts/{name}/alerts | set the alert thresholds of a host added in the panel: {cpu, memory, swap, disk, temperature, ignoreMounts}, with null for an alert that is off. A host in kogut.toml answers 409. GET /api/v1/hosts/{name} returns them as alerts, with fromFile | write |
POST /api/v1/discovered/dismiss | hide discovered items: {ids: [...]} | write |
POST /api/v1/discovered/monitor | monitor something an agent found: its service, the host it runs on, and its check, see below | write |
GET /api/v1/analytics | Site analytics for ?site=<domain>, default first site. Uses daily rollups beyond raw retention and omits metrics without full-range coverage. previous is null without full previous-period coverage. series has bucketed metrics; bounce (percent) and time (milliseconds) cover visits started in each bucket, or null without visits. dimensions has the ten top rows of each list and its total. sharedAddress flags nearly all recent page views sharing one address: forwarded is true when they named a visitor in X-Forwarded-For or X-Real-IP from an untrusted peer, and proxy is that peer’s address when it is private (RFC 1918, fc00::/7, link-local or 100.64.0.0/10), otherwise null. | read |
GET /api/v1/analytics/{list} | Site dimension: pages, referrers, countries, devices, browsers, operatingSystems, utmSources, utmMediums, or utmCampaigns. search is a case-insensitive substring. limit is 1 to 1000, default 100. total counts all matches. Pages rank by views, others by visitors. Campaign lists exclude views without that parameter. | read |
POST /api/v1/sites | Add {domain, service, check} atomically and return the tracking snippet. Optional service is created if missing; optional check is a check body for that service. | write |
GET /api/v1/settings | the settings screen, with channels, silences, and maintenance | read |
POST /api/v1/checks | add a check or heartbeat, see below | write |
GET /api/v1/drafts | checks proposed over MCP that wait for approval, see below | read |
POST /api/v1/drafts/{id}/approve, POST /api/v1/drafts/{id}/reject | approve a draft, which creates its check, or reject it | admin session only |
GET /api/v1/checks/{id} | a check’s definition: {id, managed, input} | read |
GET /api/v1/checks/{id}/detail | a check’s page: its newest runs, uptime, response times, and expiry date, see below | read |
GET /api/v1/checks/{id}/history | a page of a check’s failed runs and state changes, newest first, see below | read |
PUT /api/v1/checks/{id}, DELETE /api/v1/checks/{id} | change or delete a check added in the panel | write |
POST /api/v1/checks/{id}/ping-url | create a heartbeat’s ping URL | write |
POST /api/v1/channels | add a notification channel, see below | write |
PUT /api/v1/channels/{name}, DELETE /api/v1/channels/{name} | change or delete a channel added in the panel | write |
POST /api/v1/channels/{name}/test | send a test message to a channel, such as ntfy or ops-phone | write |
GET /api/v1/watchdog | kogut edge and the heartbeat URL: where they point, whether they come from kogut.toml, and the last call or why it failed, see below | read |
PUT /api/v1/watchdog/edge, DELETE /api/v1/watchdog/edge | set or remove kogut edge in the panel | write |
PUT /api/v1/watchdog/heartbeat-url, DELETE /api/v1/watchdog/heartbeat-url | set or remove the heartbeat URL in the panel | write |
POST /api/v1/silences, POST /api/v1/maintenance | add a silence or a maintenance window, see below | write |
PUT, DELETE /api/v1/silences/{id} and /api/v1/maintenance/{id} | change or remove one | write |
POST /api/v1/users | add a person: {email, role, password} | admin session only |
POST /api/v1/tokens | create an API token: {scope, label} | admin session only |
DELETE /api/v1/tokens/{id} | revoke a token | admin session only |
GET /api/v1/status-preview | the status page as JSON | read |
GET /api/v1/stream | server-sent events for live updates | read |
read means a session of any role or a read or write token. write means an
admin’s session or a write token. A viewer’s session gets 403 “only admins can do
this”.
Authentication
The panel uses kogut_session after password or
OpenID Connect sign-in.
Forward auth uses the proxy’s identity headers instead.
Sessions last 30 days. The cookie is HttpOnly, SameSite=Lax, and Secure with an
HTTPS public URL.
API tokens start with kg_ and go only in the Authorization: Bearer <token> header,
not in query parameters. A token takes precedence over a cookie. Missing credentials
return 401, the wrong scope 403. kogut status,
kogut incidents, and kogut checks take a read token.
| Scope | Allows |
|---|---|
read | Monitoring and settings reads under /api/v1, the event stream, and /metrics. Not session-only auth routes. |
write | everything read allows, plus acknowledging incidents, adding hosts and sites, host alert thresholds, monitoring and dismissing discovered items, checks, channels, silences, maintenance, watchdog settings, channel tests, and ping URLs. Not users or tokens. |
deploy | only POST /api/deploys and POST /api/changes |
mcp | only POST /mcp: reads, and drafts of checks that only an admin’s session approves |
The panel also offers ping and agent scopes, which no route accepts yet. Heartbeats
use their ping URL, and agents the host token from
Hosts → Add a server.
Tokens are shown once, at creation. kogut stores their SHA-256 hashes and last-use
times. Writes authenticated by a cookie or forward auth need an Origin equal to
[server] url, or to the request’s Host when no URL is set. Bearer tokens do not.
Ping URLs
POST /api/v1/checks/{id}/ping-url returns 201 with
{"url": "https://kogut.acme.com/ping/btp_...", "replaced": false}. The heartbeat’s
public id is in GET /api/v1/services/{name}. Another call invalidates the old URL and
returns replaced: true. A ping_token from kogut.toml is not replaced and keeps
working; replaced counts only URLs made here.
Checks
POST /api/v1/checks validates checks and heartbeats by the
configuration rules, with durations in milliseconds. Nullable fields
may be omitted; null uses the default:
{
"name": "API health", "service": "api", "kind": "http",
"target": "https://api.acme.com/health", "method": null,
"everyMs": 30000, "timeoutMs": null, "from": null,
"expect": [
{ "status": "2xx", "bodyContains": null, "bodyNotContains": null, "json": null,
"equals": null, "responseTimeBelowMs": null, "degraded": false }
],
"cron": null, "timezone": null, "graceMs": null
}kind is http, tcp, dns, ping, domain, or heartbeat. command exists only
on command checks from an agent’s host and is refused here
with 422. target is the URL, host:port, DNS name, ping host, or registered domain.
Ping assertions take only responseTimeBelowMs. Domain checks take neither everyMs nor
from. Heartbeats take everyMs or cron, optional timezone, and graceMs, and no
target. from names an agent host. Missing services are created.
Success returns 201 with {"id": "<public id>"} and schedules the check at once. A
name conflict returns 409, invalid values 422 with the field in the detail.
PUT /api/v1/checks/{id} takes the same body. DELETE removes the check and its history.
File-managed and command checks refuse both with 409. GET returns
{id, managed, hostDefined, input}.
POST /api/v1/discovered/monitor takes {host, service, check}, where check is the
body above or null. It atomically creates a missing service, links the host, and adds
the check, and returns 201 with {service, check}. File-managed services return 409.
Drafts
GET /api/v1/drafts returns {drafts}, oldest first. Each draft has id, check (the
check body that approving creates), proposedBy (such as token "assistant"),
createdAt, expiresAt, and problem: why approving would fail now, or null.
POST /api/v1/drafts/{id}/approve creates the check as POST /api/v1/checks does and
returns 201 with {"id": "<public id>"}. A name conflict returns 409 and invalid
values 422; the draft stays open. POST /api/v1/drafts/{id}/reject returns 204 and
creates nothing. Both return 404 for an unknown draft and 409 for one already decided
or expired. Tokens of every scope and viewers’ sessions get 403. Changes to drafts
invalidate drafts in the event stream.
A check’s runs
GET /api/v1/checks/{id}/detail accepts a range, default 24h, and returns:
| Field | Meaning |
|---|---|
check | Service check row, including id, name, kind, status, and expiry. A command check has hostDefined set, with from naming its host, lastDetail its last message, and lastValue its last number. |
service | Service name. |
beats | Newest 90 runs, oldest first: {ts, status, latencyMs, detail}. |
uptime | {day, month, year} percentages of up or degraded runs over 24 hours, 30 days, and 365 days. null without runs. Independent of the requested range. |
dayAverageMs | Mean latency of up or degraded runs over the last 24 hours. |
series | {x, stepMs, p50, p95, value} buckets for the requested range: response times, and the mean number a command check printed, null where no run printed one. value is empty when no run in the range printed a number. |
expiry is {kind, expires, seen} for the latest observed certificate or domain
registration, or null without one or after the target host, port, or domain changes.
Service check rows include it too.
GET /api/v1/checks/{id}/history accepts before in milliseconds and limit from 1 to
100, default 20. It returns {items, next}, newest first. Each item has
{ts, kind, status, title, detail, latencyMs}:
failureis a down or degraded run, kept for[retention] check_results.changeis a check or certificate state change, including before a service move.
next is the next page’s before, or null at the end. Pages never split a timestamp,
so a page can have fewer items than limit, or more when the newest timestamp alone has
more. Both endpoints return 404 for unknown ids and use service:<name>
live invalidations.
Incident timeline rows have the changed check’s id in check, or null, and for a
deploy or change who made it in by and its link in url. Incident timelines also show
changes of no service or host. The overview lists every change
in the range in changes; a service or host lists its own.
Channels
POST /api/v1/channels accepts these fields:
| Field | Meaning |
|---|---|
name | Lowercase letters, digits, and hyphens. File-style names such as ntfy and slack-2 return 422. |
kind | ntfy, webhook, slack, discord, telegram, or email. |
url | ntfy topic or webhook, Slack, or Discord URL. |
secret | ntfy token, webhook signing secret, Telegram bot token, or SMTP password. |
chatId | Telegram chat id. |
smtp, tls, username, from, to | Email settings, validated like [[alert]]. to is an address list. tls: null selects by port. Without username, no password is kept. Other kinds use null. |
allowPrivate | Applies to ntfy, webhooks, and email. |
PUT /api/v1/channels/{name} takes the same body and name. secret: null keeps the
stored secret, and for Slack and Discord url: null keeps the stored URL. An empty string
removes an optional secret. File-managed channels refuse changes with 409.
In GET /api/v1/settings, admins and write tokens get each panel channel’s settings
without secrets and with hasSecret. Viewers and read tokens get settings: null,
since URLs may contain secrets.
kogut watchdogs
| Endpoint | Body |
|---|---|
PUT /api/v1/watchdog/edge | {url, token}. token: null keeps the stored token. |
PUT /api/v1/watchdog/heartbeat-url | {url}. |
Both return 204 and send the first request at once. PUT and DELETE return 409
while kogut.toml sets the same thing.
GET /api/v1/watchdog returns {edge, heartbeat}. Each is null or an object with
{address, managed, lastSentAt, lastError, lastErrorAt}, without tokens. A heartbeat
address shows only the host. lastError stays set until a request succeeds.
Silences and maintenance
POST /api/v1/silences and POST /api/v1/maintenance take:
{ "scope": "service", "name": "api", "starts": 1760533200000, "ends": 1760536800000,
"reason": "database upgrade" }scope is all, service, or host; name is null for all. A window must end
after it starts, end in the future, and last at most 90 days. createdBy records the
caller. PUT .../{id} changes scope, times, and reason, and DELETE .../{id} removes the
window. GET /api/v1/settings lists ids under holds.
Conventions
Incidents and other objects use 16-character hexadecimal public ids, such as
3f9c2e1a7b4d0c55. Services and hosts use names. Timestamps are UTC milliseconds since
the Unix epoch.
Errors use application/problem+json with type: "about:blank", status, title, and
an optional detail. 429 means a client rate limit, and 503 that the server is at
capacity, such as a full write queue. Both include Retry-After in seconds.
| Limit | Value |
|---|---|
| Tracker events per visitor address | bursts of 30, then 2 per second |
| Sign-in and setup attempts per address | bursts of 10, then 1 per second |
| Password checks at once | 2. More return 503. |
| Channel tests at once | 1. More return 429. |
| Live update streams | 64 |
Body of POST /e | 8 KiB |
Body of POST /api/deploys, POST /api/changes, and POST /mcp | 64 KiB |
Body of POST /hooks/github | 5 MiB |
Body of POST /agent/v1/frames | 4 MiB |
Addresses come from X-Forwarded-For only behind a proxy in trusted_proxies in
kogut.toml.
Live updates
GET /api/v1/stream sends invalidations as server-sent events, at most every 500 ms:
data: {"invalidate":["incident:3f9c2e1a7b4d0c55","incidents","overview","service:api"]}"*" invalidates everything. Fetch invalidated resources again. A : ping comment
after 25 idle seconds keeps the connection open.
Metrics
GET /metrics returns Prometheus text to a session or a read or write token:
scrape_configs:
- job_name: kogut
scheme: https
authorization:
credentials: kg_...
static_configs:
- targets: ["kogut.acme.com"]It has kogut_check_runs_total, kogut_ingest_accepted_total,
kogut_ingest_rejected_total, kogut_agent_unknown_frames_total,
kogut_writer_commits_total, kogut_agents_connected,
kogut_outbox_due, and kogut_engine_up. kogut_ingest_accepted_total has an
endpoint label: tracker, ping, deploy, change, or agent.
kogut_ingest_rejected_total has a reason label: invalid, unknown_site, bot,
extension (JavaScript errors from browser extensions), rate_limited, or queue_full.