Skip to content

HTTP API

Everything the web UI does goes through /api/*, and the same API is open to your own scripts, dashboards and assistants: there is no private half. It is plain JSON over HTTP, authenticated by a session cookie (the web UI) or by an API token (everything else).

Every instance describes its own API in an OpenAPI 3.1 document:

GET /api/openapi.json

It is public — it carries no secret, no address and nothing about your monitors — and it is the exhaustive reference: every route a client can call, its parameters, request and response shapes, error responses and the scope it needs (the x-token-scope extension on each operation: public, read, write, or none for routes a token can never call). The tables further down this page are a readable summary of the same thing.

Left out on purpose: the agent protocol (/api/ingest, /api/agent/commands*, /api/agent/relay*), which only the agent speaks; the browser redirects of SSO sign-in (/api/auth/oidc/start, /api/auth/oidc/callback) and the Spotify one (/api/music/spotify/callback); and the agent downloads (/install.sh, /install.ps1, /download/*).

Load it in any OpenAPI tool:

Terminal window
# Browse it, and try calls with your token, in Swagger UI
docker run --rm -p 8081:8080 \
-e SWAGGER_JSON_URL=https://monit.lan:8080/api/openapi.json swaggerapi/swagger-ui
# then open http://localhost:8081 and paste "dmt_…" under Authorize → bearerAuth
# Generate a client library (here Python)
docker run --rm -v "$PWD:/local" openapitools/openapi-generator-cli generate \
-i https://monit.lan:8080/api/openapi.json -g python -o /local/dumbmonit-client
# Or just list what is there
curl -s https://monit.lan:8080/api/openapi.json | jq -r '.paths | keys[]'

Postman and Insomnia import it from the same URL.

Two ways in, accepted on every protected route:

  • Session cookie — what the web UI uses. Sign in with a password (or SSO) and send the HttpOnly __Host-dumbmonit_session cookie (SameSite Strict, Secure; 30 days). Every state-changing request (POST, PUT, DELETE) must also carry X-Requested-With: DumbMonit — the anti-CSRF proof the UI adds automatically; without it the request is refused with 403.
  • API token — Authorization: Bearer dmt_…, for scripts, dashboards and assistants. Created in Settings → API & assistants (the same tokens the MCP server uses) and shown once. No cookie is involved, so no X-Requested-With header is ever needed.
Terminal window
# API token: one header, nothing else
curl -H 'Authorization: Bearer dmt_…' https://monit.lan:8080/api/targets
curl -H 'Authorization: Bearer dmt_…' -X POST https://monit.lan:8080/api/targets/4/probe
# Session: sign in (204 and a Set-Cookie header), then send the cookie
curl -c cookies.txt -X POST https://monit.lan:8080/api/auth/login \
-H 'content-type: application/json' \
-d '{"username":"admin","password":"…"}'
curl -b cookies.txt https://monit.lan:8080/api/targets
curl -b cookies.txt -X POST https://monit.lan:8080/api/targets \
-H 'X-Requested-With: DumbMonit' -H 'content-type: application/json' \
-d '{"name":"NAS","address":"nas.lan","kind":"snmp","credential":{"type":"snmp_community","community":"public"}}'

With the self-signed certificate, curl refuses the connection: pass --cacert dumbmonit.pem (the certificate downloaded from Settings → System → HTTPS and encryption), or -k to skip the check on a trusted network.

When a request carries a bearer token, the cookie is ignored: a revoked token is refused even from a signed-in browser, rather than quietly falling back on the session.

Agent enrollment tokens (secrets starting with dmon_) are a different thing: they only work on the agent routes (/api/ingest, /api/agent/commands/*, /api/agent/relay*). They say that a machine may talk, not which machine: on those routes a bound machine must also present its own binding secret in X-DumbMonit-Agent-Secret (dmab_…), which the server hands out once at enrolment. See Binding.

Sign-in is rate-limited: after five failed attempts, each further attempt is refused with 429 and a Retry-After delay that doubles from 30 s up to 5 minutes. Never guess passwords in a loop.

A token has one of two scopes. There is no third, more powerful one: what an administrator does on their own account stays out of reach of every token.

read tokenwrite token
Every GET a viewer account can make (monitors, alerts, metrics, rules, channels without their secrets, status pages…)yesyes
POST, PUT, DELETE an administrator can make (monitors, rules, silences, channels, status pages, packs, probes, discovery…)403yes
/metrics, /federate, /prometheus/* (scraping)yesyes
MCP read tools / write tools (assistants)read onlyboth
Accounts, sessions, two-factor, SSO, audit log (/api/auth/*, /api/users/*)403403
API tokens and agent enrollment tokens (/api/tokens/*, /api/agent/tokens/*)403403
Backup and restore (/api/backup/*)403403

A read token on a write route gets 403 with a message naming the token and the missing scope. Because no token can list, create or revoke tokens, a token can never mint another one — let alone a more powerful one.

  • Shown once, stored hashed. A token is dmt_ followed by 128 random bits. The server keeps only its SHA-256 digest (and the first characters, dmt_1a2b3c4d, to recognise it in a list); the token in clear exists only in the response to its creation. Presented tokens are compared in constant time.
  • Expiry. Choose 30 days, 90 days, 1 year or never when creating it. An expired token gets 401 with The API token "<name>" expired on … and WWW-Authenticate: Bearer error="invalid_token"; it stays in the list, marked expired, until you revoke it.
  • Allowed networks. Optionally restrict a token to a list of addresses or CIDR ranges (192.168.1.0/24, 10.8.0.5, 32 entries at most). From anywhere else it gets 403 with The API token "<name>" cannot be used from <address>. The address checked is the TCP client’s, or — only when the connection comes from a proxy listed in DUMBMONIT_TRUSTED_PROXIES — the one that proxy reports in X-Forwarded-For. Behind a reverse proxy you have not declared, every request seems to come from the proxy: declare it, or the restriction will refuse everyone.
  • Last use. The token list shows when each token was last used and from which address (updated at most once a minute, or as soon as the address changes): a token nobody uses any more is one to revoke.
  • Bound to its creator. A token remembers the account that created it. If that account is disabled, its tokens are refused (401); if it is demoted to operator or viewer, its write tokens act as read tokens; if it is deleted, its tokens are revoked.
  • Revocation is immediate: the next call gets 401.
  • Logged. Every write made with a token is written to the server log with the token’s name and id, the method, the path (without query string) and the status — never the token, never the body. Creating and revoking a token are recorded in the audit log (Settings → Security).

Each token may make 120 calls per minute, REST and MCP together. Beyond that, calls are refused with 429 and a Retry-After header (seconds) until the minute is over. Every response to a token-authenticated request tells where you stand:

HeaderMeaning
RateLimit-LimitCalls allowed per window (120).
RateLimit-RemainingCalls left in the current window.
RateLimit-ResetSeconds until the window starts over.

A Grafana dashboard with many panels and a short refresh can reach the limit: give it its own token, or lengthen the refresh.

A page served from another origin — your own dashboard, Swagger UI, a browser-based MCP client — can call the API with a bearer token, never with a session:

  • a response carries Access-Control-Allow-Origin only when the request carried Authorization: Bearer dmt_… (or for the public GET /api/openapi.json). Access-Control-Allow-Credentials is never sent, so a browser never attaches the session cookie to a cross-origin call;
  • a preflight (OPTIONS) on /api/* succeeds only when the announced headers include authorization: 204, methods GET, POST, PUT, DELETE, headers authorization, content-type, mcp-protocol-version, mcp-method, mcp-name, cached 10 minutes. X-Requested-With is not among them, so a cookie session can never make a write from another origin;
  • Retry-After, WWW-Authenticate, the RateLimit-* headers and X-DumbMonit-Api-Version are readable by the calling script.

DUMBMONIT_API_CORS_ORIGINS sets which origins are allowed:

ValueEffect
unset (default)Any origin (Access-Control-Allow-Origin: *). Safe, since only a bearer token opens anything.
https://grafana.example.com,https://dash.lanOnly these origins, echoed back.
offNo CORS headers at all: only same-origin pages and non-browser clients.

The same list decides which Origin the MCP endpoint accepts.

// From a page on another origin. Never ship a write token in a public page.
const response = await fetch('https://monit.example.com/api/alerts', {
headers: { Authorization: 'Bearer dmt_…' }
});
if (!response.ok) throw new Error((await response.json()).error);
const alerts = await response.json();
console.log(response.headers.get('RateLimit-Remaining'), 'calls left this minute');

Every response under /api carries the API version:

X-DumbMonit-Api-Version: 0.1

The same value is info.version in the OpenAPI document, next to x-server-version, the version of the server that answered. While the API is 0.x it is not frozen; the version changes whenever a route or a field is removed or renamed. Adding a route or a field does not change it: ignore fields you do not know.

Every error is JSON: {"error": "message"}, written for a person.

StatusWhen
400The request is malformed or a value is invalid; the message names it.
401No valid session or token: missing, unknown, revoked or expired token (with WWW-Authenticate: Bearer), or a token whose creator is disabled.
403Authenticated, but not allowed: a viewer, an operator outside alert handling, or a read token on a write route, a token on an account, token or backup route, a token used from a network it is not allowed from, a cookie write without X-Requested-With, or any write on the public demo (with "demo": true).
404The id does not exist. A path under /api that matches no route answers 404 {"error": "Unknown API route: …"} rather than the web UI’s HTML.
405The method does not exist on that path (GET /api/mcp, for instance).
409A conflict with the current state (a duplicate, a command already running…).
429Rate-limited, with Retry-After.
500A server fault; the details are in the server log, never in the response.
Terminal window
$ curl -i -H 'Authorization: Bearer dmt_revoked…' https://monit.lan:8080/api/targets
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer
{"error":"A valid API token is required."}
$ curl -i -X DELETE -H 'Authorization: Bearer dmt_…read…' https://monit.lan:8080/api/targets/4
HTTP/1.1 403 Forbidden
{"error":"This action needs a token with the \"write\" scope; the token \"Grafana\" is \"read\" only. Create a write token in Settings → API & assistants."}
$ curl -i -H 'Authorization: Bearer dmt_…' https://monit.lan:8080/api/alerts # the 121st call this minute
HTTP/1.1 429 Too Many Requests
retry-after: 17
ratelimit-limit: 120
ratelimit-remaining: 0
{"error":"Rate limit reached (120 calls per minute per token). Retry in 17 s."}

The MCP endpoint answers in JSON-RPC instead, with its own error codes.

In the tables below, session means a session cookie or an API token; admin means an administrator session or a write token; operator means an operator or administrator session, or a write token; session only means a session cookie, never a token.

Roles. A viewer account makes every GET. An operator makes the same reads plus the alert-handling writes marked operator in this page (and x-roles: [admin, operator] in the OpenAPI document): acknowledge, snooze (maintenance windows), ignore a rule for a monitor, clear resolved history. Every other write is admin only and answers 403 to an operator.

MethodRouteAuthPurpose
GET/api/auth/statuspublic{"configured": bool, "authenticated": bool, "user": …, "oidc": {"enabled", "provider_name", "login_url"}}. configured: false means a fresh instance: until the first admin exists, every route marked session answers 401 — only status, setup, login and health are reachable.
POST/api/auth/setuppublic{"setup_code": "…", "username": "admin", "password": "…"}. Creates the first administrator on a fresh instance (at least 12 characters); username may be omitted and is then admin. setup_code is the one-time code printed in the server logs at startup (or DUMBMONIT_SETUP_CODE); case, dashes and spaces are ignored. 204; does not open a session. 401 for a wrong code, 429 after too many, 409 once an account exists.
POST/api/auth/loginpublic{"username": "…", "password": "…"}. 204 with Set-Cookie. 401 on a wrong password, 429 when rate-limited. When the account has two-factor enabled, 200 with {"totp_required": true, "pending": "…"} instead — a ticket that lives five minutes and dies after five wrong codes: finish with /api/auth/login/totp.
POST/api/auth/login/totppublic{"pending": "…", "code": "123456"} — the ticket from the first step and a code (or a recovery code). 204 with the session cookie.
GET/api/auth/oidc/startpublicRedirects the browser to the identity provider.
GET/api/auth/oidc/callbackpublicReturn from the provider (code, state): opens the session and redirects to the UI.
GET/api/auth/mesession onlyThe current account: id, username, display_name, role (admin, operator, viewer), auth (password, oidc), disabled, totp_enabled, created_at, last_login_at.
POST/api/auth/logoutsession onlyEnds the session and clears the cookie.
POST/api/auth/passwordsession only{"current_password": "…", "new_password": "…"}. Signs out every other session.
GET/api/auth/totpsession onlyTwo-factor state of the current account: enabled, pending, recovery_codes_left.
POST/api/auth/totp/enrollsession only{"password": "…"}. Proposes a secret: secret (base32), otpauth_uri, issuer, account. Nothing is enforced until verified.
POST/api/auth/totp/verifysession only{"code": "123456"}. Confirms the enrolment and returns {"recovery_codes": […]} — shown once.
DELETE/api/auth/totpsession only{"password": "…"}. Removes the second factor. 204.
GET/api/auth/audit?limit=200admin, session onlyThe latest security events: id, at, actor, action (login, login.failed, password.changed, token.created, user.updated, …), subject, ip.
GET/api/auth/oidc/configadmin, session onlyThe SSO settings and where they come from (environment or database); the client secret is never returned.
PUT/api/auth/oidc/configadmin, session only{"issuer", "client_id", "client_secret", "provider_name", "scopes", "auto_create", "admin_groups", "groups_claim", "public_url"}. An absent or empty client_secret keeps the stored one; an absent auto_create means off.
DELETE/api/auth/oidc/configadmin, session onlyForgets the stored SSO settings (environment variables, if any, apply again). 204.
POST/api/auth/oidc/testadmin, session onlyFetches the provider’s discovery document and reports what it found.
GET/api/usersadmin, session onlyEvery account, in the shape of /api/auth/me.
POST/api/usersadmin, session only{"username", "display_name", "role", "password"}. password may be omitted only when SSO is enabled. 201.
PUT/api/users/{id}admin, session onlyAny of display_name, role, disabled, password; an omitted field keeps its value. Cannot demote or disable the last administrator.
DELETE/api/users/{id}admin, session only204. Not yourself, not the last administrator.
DELETE/api/users/{id}/totpadmin, session onlyResets another account’s second factor (a locked-out colleague). 204.
GET/api/tokenssession onlyEvery API token: id, name, prefix, scope, created_at, created_by (the account that created it), expires_at (null: never), expired, allowed_networks (empty: anywhere), last_used_at, last_used_ip, revoked_at. Never the secret.
POST/api/tokensadmin, session only{"name": "Grafana", "scope": "read", "expires_in_days": 90, "allowed_networks": ["192.168.1.0/24"]}. name is required (80 characters at most); scope is read (default) or write; expires_in_days runs from 1 to 3650, null or absent for a token that never expires; allowed_networks takes up to 32 addresses or CIDR ranges, stored in network form (192.168.1.7/24 becomes 192.168.1.0/24), empty or absent for anywhere. 201 with the token fields plus secret (shown once). 400 names an invalid network.
DELETE/api/tokens/{id}admin, session onlyRevoke. 204; 404 when unknown or already revoked.

A token as listed:

{
"id": 3, "name": "Grafana", "prefix": "dmt_1a2b3c4d", "scope": "read",
"created_at": "2026-10-01T09:12:00Z", "created_by": "admin",
"expires_at": "2026-12-30T09:12:00Z", "expired": false,
"allowed_networks": ["192.168.1.0/24"],
"last_used_at": "2026-10-01T09:30:00Z", "last_used_ip": "192.168.1.20",
"revoked_at": null
}

Besides X-DumbMonit-Api-Version (versioning), the RateLimit-* headers (rate limits) and the CORS headers (cross-origin requests), every response carries X-Content-Type-Options: nosniff, Referrer-Policy: same-origin and a Content-Security-Policy. The policy starts from default-src 'none' and opens only what the interface really uses — all of it served by DumbMonit itself:

default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self';
script-src 'self' 'nonce-<per response>'; frame-ancestors 'none'

There is no third-party origin in it, and there never should be: the fonts, the charting library and every asset ship inside the binary. The inline scripts of the page (the theme applied before first paint, SvelteKit’s bootstrap) are allowed by a nonce regenerated for each response, so 'unsafe-inline' never applies to scripts. It does apply to styles, and cannot be removed: Svelte sets style= attributes on elements, which neither a nonce nor a hash can cover.

Public status pages are made to be embedded: /s/<slug> and its compact view /s/<slug>/embed — and only those two — get the same policy without frame-ancestors. Everything else, the subscription confirmation and unsubscribe pages under /s/ included, also carries X-Frame-Options: DENY.

If you put DumbMonit behind a reverse proxy, do not let it add a second Content-Security-Policy header: browsers enforce the intersection of all of them, and a proxy default without the nonce leaves a blank page.

MethodRouteAuthPurpose
GET/api/healthpublicServer version and the state of its dependencies. Always 200: a failing component is in the body.
GET/api/updatesession or tokenRunning version, latest release, update_available; the server asks GitHub once a day at most. POST /api/update/check looks now, PUT /api/update/settings turns the check on or off. See Update.
GET/api/upgrade-notesadmin sessionConfiguration changes a release asks for in your docker-compose.yml, with the block to copy and where this instance stands. POST /api/upgrade-notes/close closes some for good, POST /api/upgrade-notes/announce records that the window was shown. See Update.
{"status":"ok","version":"0.1.0","database":{"ok":true},"victoria":{"ok":true,"embedded":true}}

status is degraded when a component fails; that component then carries an error string. victoria.embedded is true when the server runs its own VictoriaMetrics, false when DUMBMONIT_VM_URL points at an external one.

MethodRouteAuthPurpose
GET/api/backupadmin, session onlyWhat a bundle would contain (contents, one line per section with its count), the bundle format version, where the instance secret lives (secret_source: file or environment) and the state of the scheduled local backups (schedule).
POST/api/backupadmin, session only{"passphrase": "…", "include_account_secrets": false}. Returns the bundle itself as a JSON file: a cleartext header (format, version, created_at, summary, kdf) and a payload encrypted with AES-256-GCM under a key derived from the passphrase by Argon2id. At least 16 characters, or 400.
POST/api/backup/restoreadmin, session only{"bundle": {…}, "passphrase": "…", "apply": false}. Dry run unless apply is true; both run the same code, the dry run inside a transaction that is rolled back. Answers the report: per section, created, updated, skipped and notes, plus warnings. 400 on a wrong passphrase, a modified file, or a bundle written in a newer format version.
POST/api/backup/localadmin, session onlyWrites one scheduled-style local backup right now and answers the new schedule.

A bundle carries every credential of the instance, re-encrypted with the passphrase, so these routes refuse API tokens with an explanation: only an administrator signed in to the web interface gets one. See Backup and restore.

MethodRoutePurpose
GET/api/collectorsEvery monitor kind this server can monitor: kind, label, summary, examples, credential_types, address_hint, default_port, setup (title, steps, warning, doc_url) and options (key, label, help, placeholder, default, required, input, choices). Kinds brought by an integration pack are listed the same way, as pack.<id>.

Admin session or API token (write scope for changes). See Integration packs.

MethodRoutePurpose
GET/api/packsInstalled packs: id, version, label, summary, kind (null for a pack with SNMP profiles only), enabled, installed_at, sha256, metrics, rules (uids), snmp_profiles, targets (monitors using it), warnings, and error when the stored pack no longer passes validation.
GET/api/packs/{id}One pack.
POST/api/packsInstall or update. Body: the YAML (application/yaml, text/plain…) or {"yaml": "…"} in JSON. 201 when created, 200 when updated or unchanged, with outcome (created, updated, unchanged), pack, rules_added and restart_required (SNMP profiles). 400 lists every validation error.
PUT/api/packs/{id}/enable, /api/packs/{id}/disableRegister or withdraw the pack’s monitor type; monitors, history and rules stay.
DELETE/api/packs/{id}Uninstall, with its rules. 409 while monitors use it.
MethodRoutePurpose
GET/api/targetsList every monitor.
POST/api/targetsCreate one. 201 with the monitor. Profile detection starts in the background for SNMP.
GET/api/targets/{id}One monitor.
PUT/api/targets/{id}Replace it. Omitting credential keeps the stored one; omitting profile_id keeps the detected profile ("" clears it). Setting enabled: false clears the monitor’s alerts without notifying.
DELETE/api/targets/{id}204. Clears the monitor’s alerts without notifying and deletes its time series from VictoriaMetrics (best effort).
POST/api/targets/{id}/probeProbe now: {"sample_count": 42, "series": ["if_octets_in{host=\"Core switch\",ifname=\"eth0\"}", …]} — the series keys as the collector produced them, without the dumbmonit_ prefix the writer adds on the way to VictoriaMetrics.
POST/api/targets/{id}/discoverRe-run profile detection: {"profile_id": "host-resources"} or null.

A monitor as returned:

{
"id": 4,
"name": "Core switch",
"address": "192.168.1.2",
"kind": "snmp",
"profile_id": "host-resources",
"parent_id": null,
"via_agent": null,
"interval_secs": 60,
"enabled": true,
"tags": {"role": "network"},
"credential_kind": "SNMP community",
"last_probe_at": "2026-09-15 13:20:05",
"last_error": null,
"error_kind": null
}

The secret itself is never returned: credential_kind is a label. Server timestamps are UTC without a suffix. error_kind classifies last_error: down (the monitor did not answer) or config (our side — credentials, address, option), which is what the UI shows as Unreachable or Misconfigured; null when the last probe succeeded. A service check (HTTP, TCP, NTP, WebSocket…) that ran but found the service failing records Service down: the <probe> check failed (<reason>) with error_kind down.

Creating one:

{
"name": "Core switch",
"address": "192.168.1.10",
"kind": "snmp",
"interval_secs": 60,
"parent_id": null,
"tags": {},
"credential": {"type": "snmp_community", "community": "public"}
}

Credential shapes (type): none; snmp_community (community); snmp_v3 (username, optional auth: {protocol, passphrase} with md5, sha1, sha224, sha256, sha384 or sha512, optional privacy: {protocol, passphrase} with des, aes128, aes192 or aes256, optional context); api_token (token, or the two halves token_id and secret, which the server joins into user@realm!name=secret for Proxmox VE and PBS); username_password (username, password). interval_secs defaults to 60 and cannot go below 10. name is at most 200 characters and address at most 253 (400 beyond). parent_id must name an existing monitor (400 Parent device N not found.). Type options go in tags under their key.

via_agent names the relay agent that probes the monitor from its own network instead of the server; null (the default) means the server probes it. It must be an agent monitor (400 otherwise), a monitor cannot relay itself, and an agent monitor cannot be relayed. On PUT, an omitted via_agent keeps the stored value, like profile_id and credential; send null to go back to direct probing. POST /api/targets/{id}/probe and /discover on a relayed monitor go through the relay and wait for its answer (409 when a probe is already in flight, 400 Timed out: relay agent … when it does not answer).

MethodRouteAuthPurpose
POST/api/discoveryadmin{"cidr": "192.168.1.0/24", "community": "public", "port": 161, "timeout_ms": 1000}. Scan a network for SNMP devices. cidr is required; community defaults to public; timeout_ms is clamped to 100–10,000. Networks larger than 4096 addresses are refused. The scan is a POST with a JSON body so that the community never appears in a URL or a log line.
{
"scanned": 254,
"devices": [
{"address": "192.168.1.10", "sysname": "sw-core", "sysdescr": "…", "sysobjectid": "1.3.6.1.4.1.8072.3.2.10", "suggested_profile": "host-resources"}
]
}

The three steps the overview shows on a new instance. The state is kept on the instance, not in the browser, so skipping a step skips it everywhere.

MethodRouteAuthPurpose
GET/api/onboardingsessionWhere the instance stands.
PUT/api/onboardingadmin{"skipped": true} puts the whole guide away; {"dismissed": ["test"]} puts single steps away. An omitted field keeps its stored value; a step id other than device, channel or test is a 400. Returns the same document as GET.
{
"skipped": false,
"complete": false,
"completed_at": null,
"dismissed": [],
"has_target": true,
"has_channel": true,
"notification_confirmed": false
}

has_target, has_channel and notification_confirmed are recomputed on every read: they are facts about the instance, not flags the interface sets. notification_confirmed is true once a message has actually left some channel — a test message counts, a channel that was only created does not.

complete is latched and never goes back to false. The server sets it when the three steps are settled (done, or dismissed), and also on the very first read of an instance that already had a monitor and a channel — so upgrading an established instance never shows it a first-run guide.

Both routes proxy VictoriaMetrics and return series in the Prometheus shape: [{"metric": {labels…}, "values": [[timestamp_seconds, "value"], …]}].

MethodRoutePurpose
GET/api/metrics/query?query=…Instant MetricsQL query.
GET/api/metrics/query_range?query=…&start=…&end=…&step=…Range query. start and end are milliseconds since the epoch; step is in seconds and is widened so that a series never exceeds 2,000 points.
Terminal window
curl -b cookies.txt -G https://localhost:8080/api/metrics/query \
--data-urlencode 'query=avg by (target, host) (dumbmonit_cpu_usage_percent)'
[{"metric":{"host":"ThinkpadE14","target":"3"},"values":[[1757941205,"12.5"]]}]

For an existing Prometheus or Grafana. Both take a read API token in Authorization: Bearer dmt_… (a session cookie works too); a scraper sends no X-Requested-With header and needs none — these routes only read. See the metrics reference for a copy-pastable scrape_config and the Grafana data source.

MethodRoutePurpose
GET/metricsHealth of the instance itself in the Prometheus text exposition format: scheduler, probes, writes, alerting, notifications, agents, database, VictoriaMetrics. A few dozen series, never one per monitor.
GET/federateThe measurements, in the same format. match[] selects series (repeatable, 10 at most, {__name__=~"dumbmonit_.*"} by default), max_lookback widens the window the last point is looked for in (5m by default). 400 beyond 8 MiB, with what to narrow.
GET, POST/prometheus/api/v1/{route}The read half of the Prometheus API, relayed to the store, so Grafana can use http://…/prometheus as a Prometheus data source. Only query, query_range, query_exemplars, series, labels, label/{name}/values, metadata and status/buildinfo; anything else is 404.

DUMBMONIT_METRICS_PUBLIC=true serves the three without a token — only behind a firewall, since they carry every measurement of every monitor.

OTLP over HTTP, with a write API token in Authorization: Bearer dmt_… and the target named by the X-DumbMonit-Target header (or ?target=). See OpenTelemetry source.

MethodRoutePurpose
POST/api/otlp/v1/metricsOTLP metrics, protobuf or JSON, optionally gzip. 200 with partialSuccess.rejectedDataPoints when some points were not stored; 415 for another content type, 400 for an unreadable body.
POST/api/otlp/v1/logsOTLP logs, relayed to VictoriaLogs; 503 while it is disabled.

Routes not detailed above; the OpenAPI document gives their parameters and response shapes. read and write are the token scope needed; none means no token is accepted (session only).

MethodRouteScopePurpose
POST/api/targets/import/kumawriteImport monitors from an Uptime Kuma 1.x backup (the JSON file as the body, 4 MiB at most). ?dry_run=true previews. See Services.
POST/api/targets/reorderwriteReorder monitors.
GET/api/targets/{id}/securityreadSecurity score, grade and checks of one monitor.
GET/api/security/summaryreadSecurity score of every rated monitor, worst first.
GET/api/targets/{id}/webchange/pagesreadPages followed by a website watch.
GET/api/targets/{id}/webchange/changesreadChanges found on a watched website, newest first.
GET/api/targets/{id}/webchange/changes/{change_id}readOne change, with both versions and the line diff.
GET/api/targets/{id}/webchange/snapshots/{snapshot_id}/screenshotreadThe screenshot of a snapshot.
POST/api/targets/{id}/webchange/checkwriteCheck a watched website now.
GET/api/targets/{id}/opnsense/gateways, /traffic, /healthreadOPNsense: gateways and WAN addresses; interfaces, firewall and DHCP; system, services, tunnels and firmware.
GET/api/targets/{id}/truenas/storage, /protection, /healthreadTrueNAS: pools, datasets and disks; scrubs and data protection tasks; system, alerts and services.
GET/api/targets/{id}/ad, /ad/findingsreadActive Directory: domain, controllers, privileged groups and findings; the raw security findings.
GET/api/targets/{id}/redfishreadServer hardware read from its management controller.
GET/api/targets/{id}/observabilityreadHealth of a log or metrics server.
GET/api/targets/{id}/backendreadHealth of Redis, MongoDB, RabbitMQ or CrowdSec.
GET/api/targets/{id}/proxyreadHealth of a reverse proxy or a domain registration.
GET/api/public/status/{slug}/badge.svg, /response.svg, /uptime.svgpublicOverall status, mean response time and uptime badges of a status page.
GET/api/public/status/{slug}/components/{key}/badge.svg, /uptime.svg, /response.svgpublicThe same badges for one service of the page.
GET/api/public/status/{slug}/banner.jspublicScript of the status banner for third-party websites.
GET/api/public/status/{slug}/rsspublicIncidents as an RSS feed.
GET/api/music/nowreadWhat the walls play (room monitor).
PUT, DELETE/api/music/linkwriteEvery wall plays this Spotify, Deezer or YouTube link; or stops the embedded player.
GET, DELETE/api/music/spotifyread, writeThe connected Spotify account (never a token); disconnect it and forget its tokens.
POST/api/music/spotify/authorize, /completenoneStart connecting Spotify; finish a loopback connection. Session only.
GET/api/music/spotify/tokennoneA short-lived Spotify access token for the wall’s player. Session only.
GET, PUT/api/music/speakerread, writeThe wall’s Spotify speaker as Settings shows it; rename it (admin).
POST/api/music/speaker/report, /playnoneA wall reports its speaker’s state; play the account on it. Session only.
POST/api/music/spotify/playbacknonePause or resume the account from the wall, wherever it plays. Session only.
MethodRoutePurpose
GET/api/alertsActive alerts (pending, firing, suppressed, recently resolved). Alerts of deleted or paused monitors are never listed. ?target_id= keeps only the alerts of one monitor. Dismissed alerts are left out; ?dismissed=true lists only them (dismissed, dismissed_at, dismissed_by).
GET/api/alerts/history?since=2026-09-01T00:00:00Z&limit=200&dismissed=falsePhase transitions. since is RFC 3339, default the last seven days; limit must be positive. dismissed (default false) excludes entries cleared with the two routes below; nothing is ever destroyed by a clear, only hidden from the default view.
POST/api/alerts/history/{id}/dismissClears one resolved entry from the default history view. 204. 404 on an unknown id or a still-active transition. Operator; audited.
POST/api/alerts/history/dismiss-resolvedClears every not-yet-cleared resolved entry at once (“Clear all resolved”). Returns {"dismissed": <count>}. Operator; audited.
POST/api/alerts/{fingerprint}/ackAcknowledge: {"duration_secs": 14400, "note": "…"} or {"until": "2026-09-22T18:00:00Z"} (one of the two; neither means 4 hours, at most 30 days). {"until": null} lifts it. Returns the alert. Operator; audited.
DELETE/api/alerts/{fingerprint}/ackLift the acknowledgement. Returns the alert. Operator.
POST/api/alerts/{fingerprint}/dismissDismiss (“got it”): the current occurrence leaves the active alerts and sends nothing more, resolution included, until the condition clears. No body. Returns the alert. 404 if it is no longer active. Operator; audited.
DELETE/api/alerts/{fingerprint}/dismissRestore a dismissed alert. Returns the alert. Operator; audited.

An active alert:

{
"fingerprint": "…",
"rule_uid": "disk_almost_full",
"rule_name": "Disk almost full",
"severity": "warning",
"target_id": 4,
"series_key": "…",
"labels": {"host": "Core switch", "mountpoint": "/", "target": "4"},
"phase": "firing",
"effective_phase": "suppressed",
"suppressed": true,
"suppressed_by": 2,
"silenced": false,
"learning": false,
"acked": true,
"acked_until": "2026-09-15T17:20:00Z",
"acked_by": "admin",
"ack_note": "replacing the disk",
"value": 95.96,
"score": null,
"condition_since": "2026-09-15T12:00:00Z",
"firing_since": "2026-09-15T12:15:00Z",
"last_eval_at": "2026-09-15T13:20:00Z",
"last_notified_at": null,
"notify_count": 0
}

A history entry has id, fingerprint, rule_uid, target_id, from_phase, to_phase, severity, value, notified, reason (empty, or why nothing was sent: learning: would have fired, suppressed: device 2 unreachable, maintenance window, acknowledged by admin, device removed or disabled), at and dismissed (cleared from the active view; the row itself is kept). acked is true while acked_until is in the future: reminders and escalations pause, the resolution is still notified and clears the acknowledgement. A target_id that no longer exists is shown as “(deleted monitor)” by the UI.

MethodRoutePurpose
GET/api/alerts/rulesEvery rule.
POST/api/alerts/rulesCreate. 201.
PUT/api/alerts/rules/{id}Update. A built-in rule keeps its query.
DELETE/api/alerts/rules/{id}204. Built-in rules cannot be deleted.
POST/api/alerts/rules/{id}/enable{"enabled": false}. Returns the rule.

A rule:

{
"id": 2, "uid": "cpu_high", "name": "High CPU",
"description": "CPU load sustained above 90%.",
"kind": "threshold",
"query": "avg by (target, host) (dumbmonit_cpu_load_percent or dumbmonit_proxmox_node_cpu_percent or dumbmonit_cpu_usage_percent)",
"operator": ">", "threshold": 90, "for_secs": 600,
"severity": "warning",
"selector": {"kind": "all"},
"channels": [],
"params": {"k": 3.5, "alpha": 0.05, "mad_floor_abs": 1e-6, "mad_floor_rel": 0.01, "min_samples": 3},
"unit": "%", "repeat_secs": 21600, "escalate_after_secs": 3600,
"enabled": true, "builtin": true
}

Creating one needs at least name and query; kind (threshold, anomaly, predict), operator, threshold, for_secs, severity (info, warning, critical), selector ({"kind":"all"}, {"kind":"ids","ids":[…]} or {"kind":"labels","labels":{…}}), channels (channel ids; empty means every enabled channel), unit, repeat_secs, escalate_after_secs and enabled are optional.

MethodRoutePurpose
GET/api/alerts/silencesEvery window, with active_now, active_until and next_start_at (RFC 3339, computed by the server).
POST/api/alerts/silencesCreate. 201. Operator.
DELETE/api/alerts/silences/{id}204. Operator.
{
"name": "Sunday backups",
"comment": "NAS is busy",
"target_id": 4,
"matchers": {},
"schedule": {"kind": "weekly", "days": [6], "start_minute": 120, "end_minute": 240, "timezone": "Europe/Paris"},
"enabled": true
}

A one-off schedule is {"kind": "once", "starts_at": "2026-09-20T22:00:00Z", "ends_at": "2026-09-21T02:00:00Z"}. A monthly one is {"kind": "monthly", "days": [1], "nth_weekdays": [{"nth": 1, "weekday": 6}], "start_minute": 120, "duration_minutes": 120, "timezone": "Europe/Paris"} — nth is 1 to 5, or -1 for the last one of the month; a month without that occurrence is skipped. Days are 0 = Monday … 6 = Sunday (1–31 for a monthly days); minutes are since local midnight (0–1439). On a recurring schedule, timezone (an IANA name) wins over utc_offset_minutes and is what keeps a window at its local hour across daylight-saving changes; an unknown zone name is refused.

A rule can be tuned for one monitor without touching the rule itself. {"enabled": false} is what the UI’s Ignore action (on the alert, the monitor page or the rule) writes: the monitor is dropped from this one rule for good, reversibly, without disabling the rule for anyone else.

MethodRoutePurpose
GET/api/alerts/overrides?target_id=4Every override, or those of one monitor: rule_uid, target_id, threshold, clear_threshold, enabled.
GET/api/alerts/rules/{id}/overridesThe overrides of one rule.
PUT/api/alerts/rules/{id}/overrides/{target_id}{"threshold": 95, "clear_threshold": 90, "enabled": true} — each field optional; null means “as the rule”. Creates or replaces. An operator may only send {"enabled": false} (ignore); the monitor’s threshold overrides, if any, are kept.
DELETE/api/alerts/rules/{id}/overrides/{target_id}204. For an operator, only lifts the ignore: threshold overrides stay.
GET/api/targets/{id}/rulesThe rules that watch a monitor: those whose query reads a metric it wrote in the last 24 h. {"status": "ok" | "no_data" | "unavailable", "rules": [{id, uid, name, description, severity, enabled, ignored, custom_threshold, firing}]}.
MethodRoutePurpose
GET/api/notify/policyThe global policy: batch_window_secs (0 = send at once), max_per_hour per channel (0 = unlimited), flap_events, flap_window_secs, flap_hold_secs (0 = no flap detection), public_url (links in messages; empty = DUMBMONIT_PUBLIC_URL), escalate_after_secs and escalate_channel (0 / null = no escalation).
PUT/api/notify/policySame fields, every one optional: an omitted field keeps its value. "escalate_channel": null switches escalation off. Returns the policy.
POST/api/notify/match-preview{"matcher": {…}} → which monitors a channel routing filter selects: devices (id, name, kind, tags, matched), matched, total, rules, excluded_rules.
MethodRoutePurpose
GET/api/notify/kindsEvery channel type with its settings and secrets fields (key, label, required, input, help, placeholder, options, shape, default) and a doc_url such as https://dumbmonit.readthedocs.io/en/latest/notifications/#discord.
GET/api/notify/channelsEvery channel: id, name, kind, enabled, settings, has_secret, last_error, last_sent_at, policy (min_severity, notify_resolved, min_interval_secs, quiet_hours, matcher). Secrets are never returned.
POST/api/notify/channelsCreate. 201.
PUT/api/notify/channels/{id}Update. Omitting secrets keeps the stored ones; "secrets": {} clears them. In policy, omitting matcher keeps the routing filter and "matcher": null clears it.
DELETE/api/notify/channels/{id}204.
POST/api/notify/channels/{id}/testSend a test message: {"ok": true, "message": "…"}.
{
"name": "Ops Discord",
"kind": "discord",
"enabled": true,
"settings": {"username": "DumbMonit"},
"secrets": {"webhook_url": "https://discord.com/api/webhooks/…"}
}

The exact keys per kind come from /api/notify/kinds and are documented in Notification channels.

The devices of the signed-in account (session only, viewers included; API tokens get 401). A webpush channel sends to them; see Push notifications (PWA).

MethodRoutePurpose
GET/api/webpush{"public_key": "…", "devices": […]}: the VAPID public key (base64url, the applicationServerKey to subscribe with) and the account’s devices — id, device (Firefox on Android), push_service (host), fingerprint (first 16 hex characters of SHA-256 of the endpoint), created_at, last_success_at, last_error. The endpoint, the browser keys and the private key are never returned.
POST/api/webpush/subscriptionsPushSubscription.toJSON(): {"endpoint": "https://…", "keys": {"p256dh": "…", "auth": "…"}}. 201 with the device. The endpoint must be HTTPS on a public address: private, loopback and link-local addresses get 400. The same endpoint again replaces its row.
DELETE/api/webpush/subscriptions/{id}204; 404 for a device of another account.
POST/api/webpush/test{} or {"id": 3}. Sends a test notification: {"ok", "delivered", "failed", "removed", "message"}. Subscriptions answered with 404/410 are removed.

Administrators only, GET included (the response lists recipients). Bearer tokens need the write scope for changes. See Email reports.

MethodRouteAuthPurpose
GET/api/reports/schedulesadminEvery report: id, name, enabled, frequency, weekday (0 is Monday), day_of_month, hour, timezone, recipients, channel_id, last_sent_at, last_error, next_run_at, and for SLA reports kind (digest or sla), attach_pdf, attach_csv, page_id, objective.
POST/api/reports/schedulesadminThe same writable fields; each defaults to weekly, Monday, 8:00, UTC. recipients holds 20 addresses at most and is required to enable the report; channel_id must be an smtp channel or null. 201; 409 beyond 10 reports.
GET/api/reports/schedules/{id}adminOne report.
PUT/api/reports/schedules/{id}adminFull replacement.
DELETE/api/reports/schedules/{id}admin204.
GET/api/reports/slaadminThe availability (SLA) report of a period, as a file. Query: from, to (inclusive days, YYYY-MM-DD; none: the previous calendar month; a year at most), tz (IANA, default UTC), format (pdf default, csv for the monitors, incidents_csv), objective (percent), page (status page id whose title heads the PDF), title. See SLA reports.
POST/api/reports/schedules/{id}/send-testadminEmails a preview to the saved recipients: {"sent": 2, "failed": 0}. Does not touch the schedule. 400 without a usable email channel; 409 within 30 seconds of the previous preview.
MethodRouteAuthPurpose
GET/api/status-pagessessionEvery page with its items: page (id, slug, title, description, published, theme, show_uptime_days, created_at, updated_at) and items (id, page_id, target_id, label, group_name, position).
POST/api/status-pagesadmin{"title", "slug", "description", "published", "theme", "show_uptime_days"}. title is required (120 characters at most); slug (^[a-z0-9-]{2,40}$) is derived from the title when omitted; theme is auto (default), light or dark; show_uptime_days runs from 7 to 90 (90 by default). 201; 409 on a slug already taken.
GET/api/status-pages/{id}sessionOne page with its items.
PUT/api/status-pages/{id}adminSame fields, but a full replacement rather than a patch: title is required and every omitted field goes back to its default (slug re-derived from the title, empty description, published: false, theme: auto, 90 days of uptime).
DELETE/api/status-pages/{id}admin204. The public URL stops answering.
POST, PUT/api/status-pages, /api/status-pages/{id} — look and subscriptionsadminAlso accept accent (default, blue, teal, violet, rose, amber), scenes (scene ids: paris, tokyo, newyork, dubai, no duplicates; a retired id such as venice or matterhorn is folded onto the nearest of the four; [] for none; omitted on creation: ["paris"]), simple (boolean, default false: the public page shows no scene, mascot or animation and follows the visitor’s light/dark preference), scene_rotation (visit, 1m, 10m or 1h), footer_text (plain text, 280 characters), homepage_url (http(s):// or empty), subscribe_channel_id (an smtp channel, or null to turn email subscription off) and domain (the page’s public host name, like status.example.com, or null/"" for none — see Custom domain). domain is lower-cased and loses a https:// prefix or trailing slash; a port, path or IP address is 400, the address of DumbMonit itself (its public URL, or the host the request came in on) is 400, and a name another page uses is 409. Unlike the other fields, these eight keep their stored value when omitted from a PUT. The page view returns them with logo_type (image/png, image/jpeg, image/webp or null).
GET, PUT, DELETE/api/status-pages/{id}/logosession / adminThe page’s logo. PUT takes {"data": "<base64 or data: URL>"}: PNG, JPEG or WebP recognised by content, 256 KiB at most, anything else 400. 204. GET is the editor’s preview (drafts included).
PUT/api/status-pages/{id}/accessadmin{"mode": "public" | "password" | "users", "password": "…"}: who can read the page (see Private pages). password is required the first time in password mode (8 to 200 characters), kept when omitted later, and erased by any other mode. The page view returns access_mode and has_password, never the hash.
GET/api/calendar/maintenanceadminThe private maintenance calendar: {"token", "path"}, the token created on first use.
POST/api/calendar/maintenance/rotateadminA new token; the previous address stops working.
GET/api/status-pages/{id}/subscriberssessionEmail subscribers: id, page_id, email, confirmed_at (null while pending), created_at. Their tokens never leave the server.
DELETE/api/status-pages/{id}/subscribers/{subscriber}adminRemoves an address. 204.
PUT/api/status-pages/{id}/itemsadmin[{"target_id": 4, "label": "NAS", "group_name": "Storage"}, …] — the full ordered list of monitors shown on the page.
GET/api/incidentssessionEvery incident with its updates: incident (id, page_id, title, kind, status, severity, starts_at, ends_at, created_at, updated_at) and updates.
POST/api/incidentsadmin{"title", "kind", "status", "severity", "page_id", "starts_at", "ends_at", "body"} — body is the first update. 201.
PUT/api/incidents/{id}adminSame fields; an omitted field keeps its value.
DELETE/api/incidents/{id}admin204.
GET/api/incidents/{id}/updatessessionThe timeline: id, incident_id, status, body, created_at.
POST/api/incidents/{id}/updatesadmin{"status": "monitoring", "body": "…"}. Appends an update and moves the incident to status. 201.
GET/api/public/status/{slug}publicThe JSON document a published page is built from (also badge.svg and rss under the same path). In page, show_uptime_days is the configured maximum and history_days the window actually shown (the age of the page’s data, from 7 days up to that maximum) and simple mirrors simple mode. See Status pages. On a page’s custom domain (Host equal to its domain), only this page’s /api/public/status/{slug}/… routes answer; every other route of this reference is 404 there.
GET/api/public/status/{slug}/uptime.svg, /response.svgpublicPage badges: mean uptime over ?days= 1, 7, 30 (default) or 90 — 400 beyond the page’s history — and mean response time. SVG, Cache-Control: public, max-age=60.
GET/api/public/status/{slug}/components/{key}/badge.svg, /uptime.svg, /response.svgpublicThe same three badges for one service; key is the service’s key in the public document (its label in URL form). 404 for an unknown key.
GET/api/public/status/{slug}/logopublicThe page’s logo with its image type, cacheable for a day (the document’s logo_url carries a version). 404 without one.
GET/api/public/status/{slug}/maintenance.icspublicThe page’s maintenance as an iCalendar feed (RFC 5545), 90 days back onwards. Follows the page’s access mode.
POST/api/public/status/{slug}/accesspublic{"password": "…"}: trades the password of a private page for its access cookie (dm_page_<id>, HttpOnly, 12 hours). 403 for a wrong password; 429 after repeated mistakes. A page in password or users mode answers its other public routes 401 with {"error", "access": "password" | "users"} until then.
GET/api/calendar/maintenance.ics?token=…public (token)Every maintenance of the instance as an iCalendar feed. 404 for an unknown token.
POST/api/public/status/{slug}/subscribepublic{"email": "…"}. Mails a confirmation link through the page’s SMTP channel. Always 202 with the same message, whatever the address’s state; 400 for an invalid address; 404 when the page offers no email subscription; 409 when the page cannot take more subscribers; 429 with Retry-After beyond five requests per client and sixty in all per 15 minutes.
POST/api/public/status/{slug}/confirm?token=…publicConfirms a subscription (the link of the confirmation email opens /s/{slug}/confirm, whose button posts here). 404 for an unknown or expired token.
POST/api/public/status/{slug}/unsubscribe?token=…publicRemoves a subscription; the body is ignored, so it is also the RFC 8058 one-click target of List-Unsubscribe. Always 200.
MethodRouteAuthPurpose
GET/api/agent/tokenssession onlyEvery token: id, name, prefix, created_at, last_used_at, revoked_at, max_uses (null for a fleet token), uses, expires_at.
POST/api/agent/tokensadmin, session only{"name": "Home fleet", "base_url": "https://server:8080", "reusable": false, "max_uses": null, "expires_in_days": null}. 201 with the token fields plus secret (shown once), install_linux and install_windows. base_url is the URL agents will use; it defaults to the listen address. Without reusable, the token is single use: it enrols one machine and no more. With it, max_uses caps the enrolments (null: no limit). expires_in_days stops enrolments after that many days; machines already enrolled keep reporting. 400 on a count below 1.
DELETE/api/agent/tokens/{id}admin, session onlyRevoke. 204; 404 when unknown or already revoked.
POST/api/ingestAuthorization: Bearer dmon_… + X-DumbMonit-Agent-SecretReceives a batch of samples from an agent (bodies up to 16 MB). Not meant to be called by hand. The header carries the machine’s binding secret; an agent that has none omits it, which is how it asks for one. The response adds agent_secret (the secret, returned exactly once, when the machine is bound) and bound. 403 when the machine is bound to another agent installation, or when the token can no longer enrol — the message says which, and what to do.
GET/api/agent/relay?wait=NAuthorization: Bearer dmon_… + X-DumbMonit-Agent-Key + X-DumbMonit-Agent-SecretProbes delegated to a relay agent (relay: true). Held up to wait seconds (25 at most) when nothing is pending. Each item is a command of kind probe whose args carry the target, its decrypted credential, timeout_secs and discover. Never written to disk.
POST/api/agent/relay/{id}Authorization: Bearer dmon_… + X-DumbMonit-Agent-Key + X-DumbMonit-Agent-SecretOutcome of a delegated probe: {"duration_ms", "error", "samples", "profile_id"} (bodies up to 16 MB). 204; 404 when the probe expired or belongs to another agent.
GET/api/agent/commands?wait=NAuthorization: Bearer dmon_… + X-DumbMonit-Agent-Key + X-DumbMonit-Agent-SecretCommands queued for an agent (container restart or update), held up to wait seconds when nothing is pending. Not meant to be called by hand. key alone proves nothing — it is public — so a bound machine must also present its binding secret; 403 otherwise, which is what stops one machine from taking another’s commands.
POST/api/agent/commands/{id}Authorization: Bearer dmon_… + X-DumbMonit-Agent-Key + X-DumbMonit-Agent-SecretProgress and outcome of a command, reported by the agent. result is truncated to the last 4 KB, with [truncated by the server, beginning dropped] at the front when that happens.
GET/api/relayssessionEvery agent monitor as a possible relay: id, name, site, relay (declared relay: true), last_seen_at, relayed (monitors reached through it), gateway_wanted, gateway_port, gateway_host (its gateway for the agents of its site), client_id. Relays first, then by name.
GET/api/relays/kindssessionKinds a relay agent can probe; forms only offer Reached through for those.
{
"id": 1, "name": "Home fleet", "prefix": "dmon_ab12",
"created_at": "2026-09-15 13:00:00", "last_used_at": null, "revoked_at": null,
"secret": "dmon_…",
"install_linux": "curl -sSL https://server:8080/install.sh | sh -s -- --token=dmon_… --url=https://server:8080",
"install_windows": "& ([scriptblock]::Create((irm https://server:8080/install.ps1))) -Token dmon_… -Url https://server:8080"
}

Actions on the containers of a machine that runs the agent. Every route answers 404 when the monitor is not an agent target.

MethodRoutePurpose
GET/api/targets/{id}/agentThe machine as its agent last described it: hostname, os, os_version, arch, agent_version, commands_supported, relay, site, relayed (monitors reached through this agent), last_seen_at, binding, bound, bound_at, rebind_until. 404 until an agent has reported. commands_supported is true only when the agent declared that it fetches commands (commands: true, the default of current agents); an older agent or one with commands: false gives false. binding is bound, pending (the binary can be bound and will be at its next batch) or unsupported (an agent older than binding: reinstall it).
POST/api/targets/{id}/agent/rebindAdmin. Opens a one-hour window during which this machine can bind itself again with a valid enrolment token — the way back in after a reinstall took the agent’s secret with it. {"rebind_until", "minutes"}. The window closes as soon as it is used, and the old secret stops working. 404 until an agent has reported.
GET/api/targets/{id}/containersEvery container from the last batch: name, image, up, health, restart_count, uptime_seconds, image_age_seconds, update_available, policy, last_command.
PUT/api/targets/{id}/containers/{name}/policy{"auto_restart": bool, "auto_update": bool, "prune_old_image": bool, "only_in_maintenance": bool}. Every field is optional: an omitted field keeps its stored value. Returns the full policy.
POST/api/targets/{id}/containers/{name}/restartQueue a restart. 201 with the command. 404 when name is not in the agent’s inventory; 409 when the same command is already queued or running, or when the agent cannot run commands (see commands_supported).
POST/api/targets/{id}/containers/{name}/updateQueue an update; optional {"prune": bool} (defaults to the policy). Same status codes as restart.
GET/api/targets/{id}/commandsThe last 20 commands, newest first: id, kind (container.restart, container.update), args, status, requested_by (a user, or policy), created_at, started_at, finished_at, result.
DELETE/api/targets/{id}/commands/{command_id}Cancel a queued command. 204; 409 once the agent has picked it up or when it is already closed; 404 when it does not belong to this device.

status goes queued → running → done | failed. Two other final states exist: cancelled (a person pulled the command back) and expired (the server gave up after ten minutes because no agent came to fetch it — agent stopped, too old for the command channel, or installed with commands: false). The server expires stale commands every minute on its own; a queued command never blocks a new one for longer than that.

The rows of the Guests panel of a Proxmox VE monitor, assembled from the last probe in the metrics store (four instant queries, nothing asked of the hypervisor). 404 when the monitor does not exist, 400 when it is not a proxmox target.

MethodRoutePurpose
GET/api/targets/{id}/proxmox/guestsOne object per VM or container, sorted by node then VMID: vmid, name, node, kind (qemu, lxc), status (running, stopped, paused, suspended, template, unknown), cpu_percent (of the allocated cores), cpu_count, memory_used_bytes, memory_total_bytes, memory_percent, balloon_bytes, disk_used_bytes, disk_total_bytes, disk_percent, agent (true guest agent answered, false enabled but silent, null none), network_in_bps, network_out_bps, disk_read_bps, disk_write_bps, uptime_seconds, last_backup_age_seconds, ha_state, pool (its resource pool), lock (only while a lock is held: backup, migrate…), os and ip (seen from inside the guest, refreshed hourly). Every unknown value is null — a VM without guest agent has disk_total_bytes but disk_used_bytes: null; a stopped guest keeps its sizes and loses its measurements.
[
{
"vmid": 202, "name": "nextcloud", "node": "pve2", "kind": "lxc", "status": "running",
"cpu_percent": 1.0, "cpu_count": 4,
"memory_used_bytes": 1879048192, "memory_total_bytes": 4294967296, "memory_percent": 43.75, "balloon_bytes": null,
"disk_used_bytes": 61203283968, "disk_total_bytes": 107374182400, "disk_percent": 57.0, "agent": null,
"network_in_bps": 1024.0, "network_out_bps": 512.0, "disk_read_bps": 0.0, "disk_write_bps": 2048.0,
"uptime_seconds": 3196800, "last_backup_age_seconds": 25200, "ha_state": "started",
"pool": "production", "lock": null,
"os": "Debian GNU/Linux 12 (bookworm)", "ip": "192.168.10.60"
}
]

The Nodes and Ceph sections of the same monitor page, read from the same last probe. Same status codes as above.

MethodRoutePurpose
GET/api/targets/{id}/proxmox/nodes{"nodes": […], "fencing_state", "fencing_armed"} — the HA watchdog state (armed, standby…) and whether it would really isolate a lost node, then one object per node, sorted by name: name, up, cpu_percent, cpu_iowait_percent, memory_percent, ksm_shared_bytes, rootfs_percent, uptime_seconds, version (the pve-manager release installed on that node), services_down (unit names of the daemons that are stopped or failed), interfaces_offline (interfaces set to start at boot that are not up), thin_pools (name, vg, used_percent, metadata_used_percent, size_bytes) and volume_groups (name, used_percent, size_bytes). A node that stopped answering keeps its entry with up: false.
GET/api/targets/{id}/proxmox/cephavailable (false when the cluster has no Ceph — the UI then draws no section at all), health (0 OK, 1 WARN, 2 ERR, 3 unknown), health_status, bytes_used, bytes_total, used_percent, osds_total, osds_up, osds_in, osds (name, host, device_class, up, in, used_percent, used_bytes, total_bytes, apply_latency_ms, commit_latency_ms), pools (name, used_percent, used_bytes, size, min_size, pg_num, pg_num_optimal, autoscale), filesystems (CephFS names), flags (OSD flags currently set, such as noout) and muted_checks (health checks silenced, which HEALTH_OK no longer mentions).

The panels of a pbs monitor, read from the last probe (nothing asked of the server except the task log). 404 when the monitor does not exist, 400 when it is not a pbs target.

MethodRoutePurpose
GET/api/targets/{id}/pbs/calendar?days=30&offset=120{"probed_at", "days", "offset_minutes", "groups": […]}: one entry per backup group (datastore, namespace, backup_type, backup_id, name, count, last_time, last_size, last_verified, last_success, last_failure, retention) with a days array of {date, state, runs, snapshot}. offset is the viewer’s UTC offset in minutes, so that days are cut at local midnight.
GET/api/targets/{id}/pbs/failures?days=30Failed tasks: upid, worker_type, kind, worker_id, datastore, start, end, error.
GET/api/targets/{id}/pbs/jobs{"probed_at", "jobs": […]}: sync, verify, prune and garbage-collection jobs with their schedule and last result.
GET/api/targets/{id}/pbs/healthDatastores (usage, estimated full date), disks (SMART, wearout) and ZFS pools.
GET/api/targets/{id}/pbs/tasks/{upid}/log?lines=200The log of one task, fetched from the server: {"upid", "lines": […]}; lines caps how many are returned.
GET/api/targets/{id}/pbs/disks/smart?disk=/dev/sdaSMART attributes of one disk; disk is required (400 without it).
GET/api/targets/{id}/pdm/remotesProxmox Datacenter Manager: estate totals (estate) and one row per federated instance (id, kind, reachable, error, version, version_behind, node and guest counts, memory and storage, subscription, last_collection, tasks_failed), unreachable first.
GET/api/targets/{id}/pdm/failures?days=14Tasks that failed across the estate: upid, remote, worker_type, kind, worker_id, node, start, end, error.
GET/api/targets/{id}/pdm/healthThe console host: CPU, memory, root filesystem, uptime, certificates, pending updates and subscription.
GET/api/targets/{id}/pmg/queuesProxmox Mail Gateway: the four Postfix queues in reading order (queue, messages, domains, oldest_age_seconds, top_domains, stuck), plus total_messages and a gateway-wide stuck. oldest_age_seconds is a lower bound: qshape reports age brackets.
GET/api/targets/{id}/pmg/trafficToday’s totals (mail), the recent traffic curve (recent), the spam-score histogram, the viruses caught today, and quarantine counts (quarantine). Counts only: no message content.
GET/api/targets/{id}/pmg/healthOne entry per node (system, services, signatures with family, age_seconds and stale, expiring_certificates, updates, subscription), the cluster members with their sync state, and stopped_services across nodes.
MethodRoutePurpose
GET/api/targets/{id}/synologyOverview of a synology monitor from the last probe: system (model, DSM version, uptime, temperature, CPU, memory), volumes (name, fs_type, raid_type, status, severity, total_bytes, used_bytes), disks (model, serial, kind, ssd, status, smart_status, temperature_celsius, size_bytes, remaining_life_percent, …) and sampled_at. 404 when the monitor does not exist, 400 when it is not a Synology.
GET/api/targets/{id}/synology/abbActive Backup for Business: tasks, and devices — one report per protected device with its learnt cadence in assessment (state, last_success_s, typical_interval_s, 30-day counts).
MethodRouteAuthPurpose
GET or POST/api/push/{token}none (the token)The call a job makes each time it runs. Optional ?status=up|down&msg=… (Uptime Kuma compatible). 204 empty on success, 404 for an unknown token, 400 for another status, 429 with Retry-After beyond sixty calls a minute per token. See Heartbeat.
GET/api/targets/{id}/pushsessionThe heartbeat of a push monitor: token, path (/api/push/<token>), last_seen_at, last_seen_age_secs, last_status, last_message, received_total, expected_interval_secs, grace_secs, settings_error, verdict (waiting, on_time, missed, reported_down). Created on first read. 400 when the monitor is not a heartbeat.
POST/api/targets/{id}/push/regenerateadminNew token; the previous URL answers 404 from then on. Same body as the read.

The built-in Model Context Protocol server, the one an assistant talks to. It authenticates with the API tokens above — same scopes, expiry, networks and rate limit — and never with a session.

MethodRouteAuthPurpose
POST/api/mcpAPI tokenJSON-RPC 2.0 over the Streamable HTTP transport, one request per POST, answered in JSON. Protocol 2026-07-28 (stateless: every request carries its version and client capabilities in _meta, plus the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers; server/discover), and 2025-11-25, 2025-06-18 and 2025-03-26 through the initialize handshake. No session: no Mcp-Session-Id is issued, one sent is ignored. A notification gets 202 and no body.
GET, DELETE/api/mcppublic405 with Allow: POST — there is no server-sent stream and no session to end; the body only says what this endpoint is.

tools is the only capability — no resources, no prompts, no OAuth. Of the 28 tools, a read token can call the 15 that only read (get_status, list_devices, get_device, list_alerts, alert_history, query_metrics, list_silences, list_rules, list_device_types, list_agents, list_containers, list_heartbeats, list_status_pages, list_channels, list_packs); the 13 that change something (silence_device, remove_silence, acknowledge_alert, dismiss_alert, probe_device, set_device_enabled, set_rule_enabled, add_device, discover_network, schedule_maintenance, restart_container, test_channel, post_incident) need a write token. The tools call the same code as the web UI. Versions, headers, error codes and the security notes are in Connect an assistant.

MethodRoutePurpose
GET/install.shThe Linux, macOS and FreeBSD installer. Public.
GET/install.ps1The Windows installer. Public.
GET/download/{name}dumbmonit-agent-linux-x86_64, dumbmonit-agent-linux-aarch64, dumbmonit-agent-freebsd-x86_64, dumbmonit-agent-windows-x86_64.exe, served from DUMBMONIT_AGENT_DIR; {name}.sha256 gives its checksum in sha256sum format. 404 if the file is absent. dumbmonit-agent-macos-aarch64 and dumbmonit-agent-macos-x86_64 (and their .sha256) answer 307 to the same file on the latest GitHub release. Public.

Every other path is served by the web UI, which asks you to sign in itself.