kogutdocs

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 pathWhat it isAuth
GET / and other panel pathsthe panelnone to load the panel
GET /assets/*static files of the panel, cached for a yearnone
/api/v1/...the JSON API, see belowsession or token
POST /api/deploysmark a deploy, see Deploy APItoken (deploy)
POST /api/changesmark a change such as a migration, see Change APItoken (deploy)
POST /hooks/githubthe GitHub webhook, see GitHubHMAC 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 URLthe ping token in the URL
GET /b.js (also /script.js), POST /ethe tracking script and its eventsnone, the site must be known
POST /agent/v1/frames, GET /agent/v1/pollagentshost token
POST /mcpthe MCP servertoken (mcp)
GET /healthzliveness: ok and 200 when healthy, the reason and 503 when notnone
GET /metricsPrometheus metrics about kogut itselfsession or token (read)
GET /statusthe status page on the panel’s own addressnone
POST /status/subscribesubscribes the form’s email to email updates and sends a confirmation link on both hostsnone
GET/POST /status/confirm?token=...a page with a button, and the confirmation it poststhe 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 appsthe token
any other path on the status domainthe status page, only GET and HEADnone

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 pathWhat it doesWho
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 inanyone
POST /api/v1/auth/setupcreate the first account with the setup token: {token, email, password}anyone with the setup token
POST /api/v1/auth/loginsign in: {email, password}. Sets the session cookieanyone
POST /api/v1/auth/logoutsign outsession
GET /api/v1/auth/methe signed-in user: {email, role}session
GET /api/v1/config/statusthe current kogut.toml error, or nullread
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 valueswrite
GET /api/v1/overviewthe overview screen, default range 24hread
GET /api/v1/warningswhat 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 serviceread
GET /api/v1/incidents, /incidents/{id}open incidents and those resolved in the range (default 30d, at most 200), and one incidentread
POST /api/v1/incidents/{id}/ackacknowledge an incident, which stops reminderswrite
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 hourread
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 runread
POST /api/v1/hostsadd a host, or replace its token: {name}. Returns the host token bth_... and the install command oncewrite
PUT /api/v1/hosts/{name}/alertsset 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 fromFilewrite
POST /api/v1/discovered/dismisshide discovered items: {ids: [...]}write
POST /api/v1/discovered/monitormonitor something an agent found: its service, the host it runs on, and its check, see belowwrite
GET /api/v1/analyticsSite 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/sitesAdd {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/settingsthe settings screen, with channels, silences, and maintenanceread
POST /api/v1/checksadd a check or heartbeat, see belowwrite
GET /api/v1/draftschecks proposed over MCP that wait for approval, see belowread
POST /api/v1/drafts/{id}/approve, POST /api/v1/drafts/{id}/rejectapprove a draft, which creates its check, or reject itadmin session only
GET /api/v1/checks/{id}a check’s definition: {id, managed, input}read
GET /api/v1/checks/{id}/detaila check’s page: its newest runs, uptime, response times, and expiry date, see belowread
GET /api/v1/checks/{id}/historya page of a check’s failed runs and state changes, newest first, see belowread
PUT /api/v1/checks/{id}, DELETE /api/v1/checks/{id}change or delete a check added in the panelwrite
POST /api/v1/checks/{id}/ping-urlcreate a heartbeat’s ping URLwrite
POST /api/v1/channelsadd a notification channel, see belowwrite
PUT /api/v1/channels/{name}, DELETE /api/v1/channels/{name}change or delete a channel added in the panelwrite
POST /api/v1/channels/{name}/testsend a test message to a channel, such as ntfy or ops-phonewrite
GET /api/v1/watchdogkogut edge and the heartbeat URL: where they point, whether they come from kogut.toml, and the last call or why it failed, see belowread
PUT /api/v1/watchdog/edge, DELETE /api/v1/watchdog/edgeset or remove kogut edge in the panelwrite
PUT /api/v1/watchdog/heartbeat-url, DELETE /api/v1/watchdog/heartbeat-urlset or remove the heartbeat URL in the panelwrite
POST /api/v1/silences, POST /api/v1/maintenanceadd a silence or a maintenance window, see belowwrite
PUT, DELETE /api/v1/silences/{id} and /api/v1/maintenance/{id}change or remove onewrite
POST /api/v1/usersadd a person: {email, role, password}admin session only
POST /api/v1/tokenscreate an API token: {scope, label}admin session only
DELETE /api/v1/tokens/{id}revoke a tokenadmin session only
GET /api/v1/status-previewthe status page as JSONread
GET /api/v1/streamserver-sent events for live updatesread

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.

ScopeAllows
readMonitoring and settings reads under /api/v1, the event stream, and /metrics. Not session-only auth routes.
writeeverything 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.
deployonly POST /api/deploys and POST /api/changes
mcponly 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:

FieldMeaning
checkService 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.
serviceService name.
beatsNewest 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.
dayAverageMsMean 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}:

  • failure is a down or degraded run, kept for [retention] check_results.
  • change is 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:

FieldMeaning
nameLowercase letters, digits, and hyphens. File-style names such as ntfy and slack-2 return 422.
kindntfy, webhook, slack, discord, telegram, or email.
urlntfy topic or webhook, Slack, or Discord URL.
secretntfy token, webhook signing secret, Telegram bot token, or SMTP password.
chatIdTelegram chat id.
smtp, tls, username, from, toEmail settings, validated like [[alert]]. to is an address list. tls: null selects by port. Without username, no password is kept. Other kinds use null.
allowPrivateApplies 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

EndpointBody
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.

LimitValue
Tracker events per visitor addressbursts of 30, then 2 per second
Sign-in and setup attempts per addressbursts of 10, then 1 per second
Password checks at once2. More return 503.
Channel tests at once1. More return 429.
Live update streams64
Body of POST /e8 KiB
Body of POST /api/deploys, POST /api/changes, and POST /mcp64 KiB
Body of POST /hooks/github5 MiB
Body of POST /agent/v1/frames4 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:

prometheus.yml
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.