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).
OpenAPI description
Section titled “OpenAPI description”Every instance describes its own API in an OpenAPI 3.1 document:
GET /api/openapi.jsonIt 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:
# Browse it, and try calls with your token, in Swagger UIdocker 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 therecurl -s https://monit.lan:8080/api/openapi.json | jq -r '.paths | keys[]'Postman and Insomnia import it from the same URL.
Authentication
Section titled “Authentication”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_sessioncookie (SameSite Strict,Secure; 30 days). Every state-changing request (POST,PUT,DELETE) must also carryX-Requested-With: DumbMonit— the anti-CSRF proof the UI adds automatically; without it the request is refused with403. - 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 noX-Requested-Withheader is ever needed.
# API token: one header, nothing elsecurl -H 'Authorization: Bearer dmt_…' https://monit.lan:8080/api/targetscurl -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 cookiecurl -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/targetscurl -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.
Scopes
Section titled “Scopes”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 token | write token | |
|---|---|---|
Every GET a viewer account can make (monitors, alerts, metrics, rules, channels without their secrets, status pages…) | yes | yes |
POST, PUT, DELETE an administrator can make (monitors, rules, silences, channels, status pages, packs, probes, discovery…) | 403 | yes |
/metrics, /federate, /prometheus/* (scraping) | yes | yes |
| MCP read tools / write tools (assistants) | read only | both |
Accounts, sessions, two-factor, SSO, audit log (/api/auth/*, /api/users/*) | 403 | 403 |
API tokens and agent enrollment tokens (/api/tokens/*, /api/agent/tokens/*) | 403 | 403 |
Backup and restore (/api/backup/*) | 403 | 403 |
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.
Token security
Section titled “Token security”- 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
401withThe API token "<name>" expired on …andWWW-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 gets403withThe 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 inDUMBMONIT_TRUSTED_PROXIES— the one that proxy reports inX-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, itswritetokens act asreadtokens; 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).
Rate limits
Section titled “Rate limits”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:
| Header | Meaning |
|---|---|
RateLimit-Limit | Calls allowed per window (120). |
RateLimit-Remaining | Calls left in the current window. |
RateLimit-Reset | Seconds 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.
Cross-origin requests (CORS)
Section titled “Cross-origin requests (CORS)”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-Originonly when the request carriedAuthorization: Bearer dmt_…(or for the publicGET /api/openapi.json).Access-Control-Allow-Credentialsis 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 includeauthorization:204, methodsGET, POST, PUT, DELETE, headersauthorization, content-type, mcp-protocol-version, mcp-method, mcp-name, cached 10 minutes.X-Requested-Withis not among them, so a cookie session can never make a write from another origin; Retry-After,WWW-Authenticate, theRateLimit-*headers andX-DumbMonit-Api-Versionare readable by the calling script.
DUMBMONIT_API_CORS_ORIGINS sets which origins are allowed:
| Value | Effect |
|---|---|
| unset (default) | Any origin (Access-Control-Allow-Origin: *). Safe, since only a bearer token opens anything. |
https://grafana.example.com,https://dash.lan | Only these origins, echoed back. |
off | No 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');Versioning
Section titled “Versioning”Every response under /api carries the API version:
X-DumbMonit-Api-Version: 0.1The 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.
Errors
Section titled “Errors”Every error is JSON: {"error": "message"}, written for a person.
| Status | When |
|---|---|
400 | The request is malformed or a value is invalid; the message names it. |
401 | No valid session or token: missing, unknown, revoked or expired token (with WWW-Authenticate: Bearer), or a token whose creator is disabled. |
403 | Authenticated, 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). |
404 | The 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. |
405 | The method does not exist on that path (GET /api/mcp, for instance). |
409 | A conflict with the current state (a duplicate, a command already running…). |
429 | Rate-limited, with Retry-After. |
500 | A server fault; the details are in the server log, never in the response. |
$ curl -i -H 'Authorization: Bearer dmt_revoked…' https://monit.lan:8080/api/targetsHTTP/1.1 401 Unauthorizedwww-authenticate: Bearer{"error":"A valid API token is required."}
$ curl -i -X DELETE -H 'Authorization: Bearer dmt_…read…' https://monit.lan:8080/api/targets/4HTTP/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 minuteHTTP/1.1 429 Too Many Requestsretry-after: 17ratelimit-limit: 120ratelimit-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.
Accounts, sign-in and tokens
Section titled “Accounts, sign-in and tokens”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.
| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/auth/status | public | {"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/setup | public | {"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/login | public | {"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/totp | public | {"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/start | public | Redirects the browser to the identity provider. |
GET | /api/auth/oidc/callback | public | Return from the provider (code, state): opens the session and redirects to the UI. |
GET | /api/auth/me | session only | The current account: id, username, display_name, role (admin, operator, viewer), auth (password, oidc), disabled, totp_enabled, created_at, last_login_at. |
POST | /api/auth/logout | session only | Ends the session and clears the cookie. |
POST | /api/auth/password | session only | {"current_password": "…", "new_password": "…"}. Signs out every other session. |
GET | /api/auth/totp | session only | Two-factor state of the current account: enabled, pending, recovery_codes_left. |
POST | /api/auth/totp/enroll | session only | {"password": "…"}. Proposes a secret: secret (base32), otpauth_uri, issuer, account. Nothing is enforced until verified. |
POST | /api/auth/totp/verify | session only | {"code": "123456"}. Confirms the enrolment and returns {"recovery_codes": […]} — shown once. |
DELETE | /api/auth/totp | session only | {"password": "…"}. Removes the second factor. 204. |
GET | /api/auth/audit?limit=200 | admin, session only | The latest security events: id, at, actor, action (login, login.failed, password.changed, token.created, user.updated, …), subject, ip. |
GET | /api/auth/oidc/config | admin, session only | The SSO settings and where they come from (environment or database); the client secret is never returned. |
PUT | /api/auth/oidc/config | admin, 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/config | admin, session only | Forgets the stored SSO settings (environment variables, if any, apply again). 204. |
POST | /api/auth/oidc/test | admin, session only | Fetches the provider’s discovery document and reports what it found. |
GET | /api/users | admin, session only | Every account, in the shape of /api/auth/me. |
POST | /api/users | admin, session only | {"username", "display_name", "role", "password"}. password may be omitted only when SSO is enabled. 201. |
PUT | /api/users/{id} | admin, session only | Any 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 only | 204. Not yourself, not the last administrator. |
DELETE | /api/users/{id}/totp | admin, session only | Resets another account’s second factor (a locked-out colleague). 204. |
GET | /api/tokens | session only | Every 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/tokens | admin, 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 only | Revoke. 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}Response headers
Section titled “Response headers”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.
Health
Section titled “Health”| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/health | public | Server version and the state of its dependencies. Always 200: a failing component is in the body. |
GET | /api/update | session or token | Running 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-notes | admin session | Configuration 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.
Backup and restore
Section titled “Backup and restore”| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/backup | admin, session only | What 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/backup | admin, 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/restore | admin, 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/local | admin, session only | Writes 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.
Monitor types
Section titled “Monitor types”| Method | Route | Purpose |
|---|---|---|
GET | /api/collectors | Every 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>. |
Integration packs
Section titled “Integration packs”Admin session or API token (write scope for changes). See
Integration packs.
| Method | Route | Purpose |
|---|---|---|
GET | /api/packs | Installed 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/packs | Install 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}/disable | Register 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. |
Targets (monitors)
Section titled “Targets (monitors)”| Method | Route | Purpose |
|---|---|---|
GET | /api/targets | List every monitor. |
POST | /api/targets | Create 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}/probe | Probe 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}/discover | Re-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).
Discovery
Section titled “Discovery”| Method | Route | Auth | Purpose |
|---|---|---|---|
POST | /api/discovery | admin | {"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"} ]}First-run guide
Section titled “First-run guide”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.
| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/onboarding | session | Where the instance stands. |
PUT | /api/onboarding | admin | {"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.
Metrics
Section titled “Metrics”Both routes proxy VictoriaMetrics and return series in the Prometheus shape:
[{"metric": {labels…}, "values": [[timestamp_seconds, "value"], …]}].
| Method | Route | Purpose |
|---|---|---|
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. |
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"]]}]Scraping (outside /api)
Section titled “Scraping (outside /api)”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.
| Method | Route | Purpose |
|---|---|---|
GET | /metrics | Health 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 | /federate | The 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.
Receiving OpenTelemetry (outside /api)
Section titled “Receiving OpenTelemetry (outside /api)”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.
| Method | Route | Purpose |
|---|---|---|
POST | /api/otlp/v1/metrics | OTLP 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/logs | OTLP logs, relayed to VictoriaLogs; 503 while it is disabled. |
Further routes
Section titled “Further routes”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).
| Method | Route | Scope | Purpose |
|---|---|---|---|
POST | /api/targets/import/kuma | write | Import 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/reorder | write | Reorder monitors. |
GET | /api/targets/{id}/security | read | Security score, grade and checks of one monitor. |
GET | /api/security/summary | read | Security score of every rated monitor, worst first. |
GET | /api/targets/{id}/webchange/pages | read | Pages followed by a website watch. |
GET | /api/targets/{id}/webchange/changes | read | Changes found on a watched website, newest first. |
GET | /api/targets/{id}/webchange/changes/{change_id} | read | One change, with both versions and the line diff. |
GET | /api/targets/{id}/webchange/snapshots/{snapshot_id}/screenshot | read | The screenshot of a snapshot. |
POST | /api/targets/{id}/webchange/check | write | Check a watched website now. |
GET | /api/targets/{id}/opnsense/gateways, /traffic, /health | read | OPNsense: gateways and WAN addresses; interfaces, firewall and DHCP; system, services, tunnels and firmware. |
GET | /api/targets/{id}/truenas/storage, /protection, /health | read | TrueNAS: pools, datasets and disks; scrubs and data protection tasks; system, alerts and services. |
GET | /api/targets/{id}/ad, /ad/findings | read | Active Directory: domain, controllers, privileged groups and findings; the raw security findings. |
GET | /api/targets/{id}/redfish | read | Server hardware read from its management controller. |
GET | /api/targets/{id}/observability | read | Health of a log or metrics server. |
GET | /api/targets/{id}/backend | read | Health of Redis, MongoDB, RabbitMQ or CrowdSec. |
GET | /api/targets/{id}/proxy | read | Health of a reverse proxy or a domain registration. |
GET | /api/public/status/{slug}/badge.svg, /response.svg, /uptime.svg | public | Overall status, mean response time and uptime badges of a status page. |
GET | /api/public/status/{slug}/components/{key}/badge.svg, /uptime.svg, /response.svg | public | The same badges for one service of the page. |
GET | /api/public/status/{slug}/banner.js | public | Script of the status banner for third-party websites. |
GET | /api/public/status/{slug}/rss | public | Incidents as an RSS feed. |
GET | /api/music/now | read | What the walls play (room monitor). |
PUT, DELETE | /api/music/link | write | Every wall plays this Spotify, Deezer or YouTube link; or stops the embedded player. |
GET, DELETE | /api/music/spotify | read, write | The connected Spotify account (never a token); disconnect it and forget its tokens. |
POST | /api/music/spotify/authorize, /complete | none | Start connecting Spotify; finish a loopback connection. Session only. |
GET | /api/music/spotify/token | none | A short-lived Spotify access token for the wall’s player. Session only. |
GET, PUT | /api/music/speaker | read, write | The wall’s Spotify speaker as Settings shows it; rename it (admin). |
POST | /api/music/speaker/report, /play | none | A wall reports its speaker’s state; play the account on it. Session only. |
POST | /api/music/spotify/playback | none | Pause or resume the account from the wall, wherever it plays. Session only. |
Alerts
Section titled “Alerts”| Method | Route | Purpose |
|---|---|---|
GET | /api/alerts | Active 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=false | Phase 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}/dismiss | Clears 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-resolved | Clears every not-yet-cleared resolved entry at once (“Clear all resolved”). Returns {"dismissed": <count>}. Operator; audited. |
POST | /api/alerts/{fingerprint}/ack | Acknowledge: {"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}/ack | Lift the acknowledgement. Returns the alert. Operator. |
POST | /api/alerts/{fingerprint}/dismiss | Dismiss (“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}/dismiss | Restore 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.
| Method | Route | Purpose |
|---|---|---|
GET | /api/alerts/rules | Every rule. |
POST | /api/alerts/rules | Create. 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.
Silences (maintenance windows)
Section titled “Silences (maintenance windows)”| Method | Route | Purpose |
|---|---|---|
GET | /api/alerts/silences | Every window, with active_now, active_until and next_start_at (RFC 3339, computed by the server). |
POST | /api/alerts/silences | Create. 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.
Per-monitor overrides
Section titled “Per-monitor overrides”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.
| Method | Route | Purpose |
|---|---|---|
GET | /api/alerts/overrides?target_id=4 | Every override, or those of one monitor: rule_uid, target_id, threshold, clear_threshold, enabled. |
GET | /api/alerts/rules/{id}/overrides | The 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}/rules | The 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}]}. |
Notification policy
Section titled “Notification policy”| Method | Route | Purpose |
|---|---|---|
GET | /api/notify/policy | The 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/policy | Same 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. |
Notification channels
Section titled “Notification channels”| Method | Route | Purpose |
|---|---|---|
GET | /api/notify/kinds | Every 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/channels | Every 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/channels | Create. 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}/test | Send 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.
Web Push devices
Section titled “Web Push devices”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).
| Method | Route | Purpose |
|---|---|---|
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/subscriptions | PushSubscription.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. |
Email reports
Section titled “Email reports”Administrators only, GET included (the response lists recipients). Bearer
tokens need the write scope for changes. See Email reports.
| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/reports/schedules | admin | Every 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/schedules | admin | The 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} | admin | One report. |
PUT | /api/reports/schedules/{id} | admin | Full replacement. |
DELETE | /api/reports/schedules/{id} | admin | 204. |
GET | /api/reports/sla | admin | The 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-test | admin | Emails 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. |
Status pages and incidents
Section titled “Status pages and incidents”| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/status-pages | session | Every 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-pages | admin | {"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} | session | One page with its items. |
PUT | /api/status-pages/{id} | admin | Same 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} | admin | 204. The public URL stops answering. |
POST, PUT | /api/status-pages, /api/status-pages/{id} — look and subscriptions | admin | Also 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}/logo | session / admin | The 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}/access | admin | {"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/maintenance | admin | The private maintenance calendar: {"token", "path"}, the token created on first use. |
POST | /api/calendar/maintenance/rotate | admin | A new token; the previous address stops working. |
GET | /api/status-pages/{id}/subscribers | session | Email 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} | admin | Removes an address. 204. |
PUT | /api/status-pages/{id}/items | admin | [{"target_id": 4, "label": "NAS", "group_name": "Storage"}, …] — the full ordered list of monitors shown on the page. |
GET | /api/incidents | session | Every incident with its updates: incident (id, page_id, title, kind, status, severity, starts_at, ends_at, created_at, updated_at) and updates. |
POST | /api/incidents | admin | {"title", "kind", "status", "severity", "page_id", "starts_at", "ends_at", "body"} — body is the first update. 201. |
PUT | /api/incidents/{id} | admin | Same fields; an omitted field keeps its value. |
DELETE | /api/incidents/{id} | admin | 204. |
GET | /api/incidents/{id}/updates | session | The timeline: id, incident_id, status, body, created_at. |
POST | /api/incidents/{id}/updates | admin | {"status": "monitoring", "body": "…"}. Appends an update and moves the incident to status. 201. |
GET | /api/public/status/{slug} | public | The 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.svg | public | Page 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.svg | public | The 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}/logo | public | The 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.ics | public | The page’s maintenance as an iCalendar feed (RFC 5545), 90 days back onwards. Follows the page’s access mode. |
POST | /api/public/status/{slug}/access | public | {"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}/subscribe | public | {"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=… | public | Confirms 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=… | public | Removes a subscription; the body is ignored, so it is also the RFC 8058 one-click target of List-Unsubscribe. Always 200. |
Agent tokens and ingest
Section titled “Agent tokens and ingest”| Method | Route | Auth | Purpose |
|---|---|---|---|
GET | /api/agent/tokens | session only | Every token: id, name, prefix, created_at, last_used_at, revoked_at, max_uses (null for a fleet token), uses, expires_at. |
POST | /api/agent/tokens | admin, 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 only | Revoke. 204; 404 when unknown or already revoked. |
POST | /api/ingest | Authorization: Bearer dmon_… + X-DumbMonit-Agent-Secret | Receives 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=N | Authorization: Bearer dmon_… + X-DumbMonit-Agent-Key + X-DumbMonit-Agent-Secret | Probes 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-Secret | Outcome 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=N | Authorization: Bearer dmon_… + X-DumbMonit-Agent-Key + X-DumbMonit-Agent-Secret | Commands 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-Secret | Progress 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/relays | session | Every 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/kinds | session | Kinds 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"}Containers and commands (agent monitors)
Section titled “Containers and commands (agent monitors)”Actions on the containers of a machine that runs the agent. Every route
answers 404 when the monitor is not an agent target.
| Method | Route | Purpose |
|---|---|---|
GET | /api/targets/{id}/agent | The 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/rebind | Admin. 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}/containers | Every 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}/restart | Queue 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}/update | Queue an update; optional {"prune": bool} (defaults to the policy). Same status codes as restart. |
GET | /api/targets/{id}/commands | The 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.
Proxmox VE guests
Section titled “Proxmox VE guests”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.
| Method | Route | Purpose |
|---|---|---|
GET | /api/targets/{id}/proxmox/guests | One 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" }]Proxmox VE nodes and Ceph
Section titled “Proxmox VE nodes and Ceph”The Nodes and Ceph sections of the same monitor page, read from the same last probe. Same status codes as above.
| Method | Route | Purpose |
|---|---|---|
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/ceph | available (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). |
Proxmox Backup Server
Section titled “Proxmox Backup Server”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.
| Method | Route | Purpose |
|---|---|---|
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=30 | Failed 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/health | Datastores (usage, estimated full date), disks (SMART, wearout) and ZFS pools. |
GET | /api/targets/{id}/pbs/tasks/{upid}/log?lines=200 | The 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/sda | SMART attributes of one disk; disk is required (400 without it). |
GET | /api/targets/{id}/pdm/remotes | Proxmox 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=14 | Tasks that failed across the estate: upid, remote, worker_type, kind, worker_id, node, start, end, error. |
GET | /api/targets/{id}/pdm/health | The console host: CPU, memory, root filesystem, uptime, certificates, pending updates and subscription. |
GET | /api/targets/{id}/pmg/queues | Proxmox 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/traffic | Today’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/health | One 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. |
Synology
Section titled “Synology”| Method | Route | Purpose |
|---|---|---|
GET | /api/targets/{id}/synology | Overview 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/abb | Active 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). |
Heartbeats (push monitors)
Section titled “Heartbeats (push monitors)”| Method | Route | Auth | Purpose |
|---|---|---|---|
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}/push | session | The 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/regenerate | admin | New token; the previous URL answers 404 from then on. Same body as the read. |
Assistants (MCP)
Section titled “Assistants (MCP)”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.
| Method | Route | Auth | Purpose |
|---|---|---|---|
POST | /api/mcp | API token | JSON-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/mcp | public | 405 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.
Agent files (outside /api)
Section titled “Agent files (outside /api)”| Method | Route | Purpose |
|---|---|---|
GET | /install.sh | The Linux, macOS and FreeBSD installer. Public. |
GET | /install.ps1 | The 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.