Uptimer v2.0.0-preview documentation — it describes the release candidate, and v1.8.0 remains the current release. Commands here pull the preview image :2.0.0-rc2@sha256:1dcea73dc2b9906e3e0f1f0d5a95eb585a4ea1f7e89fc1f2cc43bd7d14773557. The Python SDK for this preview is a local wheel, not a PyPI release. Go to the current release (v1.8.0) →
Reference › API v3

API v3

The public API of Uptimer 2.0: Resources, Templates, Observations, Incidents, maintenance and acknowledgement.

The public API of Uptimer 2.0: Resources, Templates, Observations, Incidents, maintenance and acknowledgement. The Python SDK 2.0 (uptimer-python-sdk) wraps every route here. uptimer mcp offers it to MCP clients (MCP).

Calling it

Base path /api/v3. Send a person’s API key (User → API keys):

Authorization: Bearer <api key>

A full key reads and writes what its owner may in each Workspace: every member reads; creating, changing, sending Observations and maintenance need editor or owner; any member may acknowledge. An Observation address is not a key and reads nothing here.

A scoped key (User → API keys → Access: Scoped) reaches one Workspace. It reads everything there and takes only the actions it was given: acknowledge, maintenance (start and end), observe (send Observations), templates (publish Templates), resources (create and archive Resources). It never edits a Resource, and it reads a Resource’s secret Template fields as [redacted], its URL fields without credentials, and an Observation label whose name marks a credential (api_token, Authorization, …) as [redacted]; Template field defaults follow the same rule. Its owner’s role still applies on top: a viewer’s scoped key cannot start maintenance. Any other Workspace answers 404. A scoped key is not accepted as the second factor of an Observation address. uptimer mcp uses these keys; see MCP.

Every write, refused or not, and every read of recorded evidence is kept in server_api_audit: time, person, key, Workspace, method, path, status and User-Agent (uptimer-mcp/<version> for MCP calls). Recorded evidence means an Incident with its history (GET …/incidents/{id}), its deliveries (…/incidents/{id}/deliveries) and the Observation log (GET …/resources/{r}/observations). Lists of Workspaces, Templates, Locations, Resources and Incidents, and a Resource’s detail, are not recorded.

Identifiers are public ids (short strings). A Resource is also addressed by its key.

Answers and errors

Every answer is one envelope:

{"result": …, "error": null, "meta": null}
{"result": null, "error": {"code": 1422, "error_type": "validation", "message": "…", "details": {"field": "url"}}, "meta": null}
HTTPcodeerror_typeMeaning
4001400bad_requestThe body is not JSON this route reads (unknown fields are refused).
4011401authNo key, or one this installation does not accept.
4031403forbiddenA member whose role does not allow this write, or a scoped key without the action; then details.scope names it (acknowledge, maintenance, observe, templates, resources, or full).
4041404not_foundNo such route, Workspace, Resource or Incident — including one in a Workspace you do not belong to.
4091409conflictNot in a state the action applies to (acknowledging a closed or acknowledged Incident).
4221422validationA field was refused; details.field names it.
5001500serverThis installation failed.

The 1.x API families (/api/v1/*, /api/v2/workspaces, …) answer 410 with code 1410.

Lists that page put the next cursor in meta.next_cursor (null on the last page). Pass it back as cursor. limit is 1–200, default 50.

Routes

Method and pathResult
GET /version (no key){version, api: "v3"}
GET /keywhat this key may do: {user, access, workspace}; access is ["full"] or ["read", …actions], workspace is null on a full key
GET /workspacesthe Workspaces this key reaches: [{id, name, role}]
GET /templatesthe system Templates with fields, signals, rules
GET /locations[{id, name}]
GET /workspaces/{ws}/templatesthe system Templates, then this Workspace’s own, every revision (id is key@version); each Rule carries its action and destination
POST /workspaces/{ws}/templatespublish a pushed-data Template revision (below) → 201; 409 if this key and version exist; editor or owner, full key
GET /workspaces/{ws}/resourcespage of Resources by id, with open_incident and archived_at; state active (default), archived or all; template; meta.<field>=<value> (below); limit 1–200, cursor
POST /workspaces/{ws}/resourcescreate from a Template: {template, key?, name, meta} → 201, Resource detail. template is a key (its newest revision) or key@version
GET /workspaces/{ws}/resources/{id or key}Resource detail: signals, rules (each with status, explanation, since, open_incident, action), maintenance
PATCH /workspaces/{ws}/resources/{id or key}{name?, meta?}; unsent answers stay; key and Template never change
POST /workspaces/{ws}/resources/{id or key}/archiveretire it from the inventory → 200, Resource detail with archived_at; 409 if already archived; editor or owner, full key
POST /workspaces/{ws}/resources/{r}/observations{signal, state, kind?, value?, labels?, body?, at?, id?} → 202 {resource, signal, observation, created_signal}; state is ok, problem or no_data (no evidence this time; never health); the same id twice is stored once
GET /workspaces/{ws}/resources/{r}/observations?signal&limitnewest logged Observations — context, not a decision record
PUT /workspaces/{ws}/resources/{r}/maintenance{minutes}: hold notifications; judging and history go on
DELETE /workspaces/{ws}/resources/{r}/maintenanceend it
GET /workspaces/{ws}/incidentspage of Incidents, newest first; filters resource, rule, lifecycle (open, closed), confirmation (confirmed, unconfirmed), and the Resources’ template, resource_state (all by default, active, archived) and meta.<field>
GET /workspaces/{ws}/resources/{r}/incidentsthe same, for one Resource
GET /workspaces/{ws}/incidents/{id}Incident with history, oldest first
GET /workspaces/{ws}/incidents/{id}/deliveries?limitwhat was sent about it, newest first: [{at, destination, type, event, status, reason}]; status delivered, failed or held; reason a fixed code (below), null when delivered
POST /workspaces/{ws}/incidents/{id}/acknowledgetake it on; 409 if closed or already taken

A delivery reason is one of unreachable, http_NNN (the status code the destination answered), not_sent (it could not be prepared), maintenance, no_destination, destination_disabled (the Rule’s own destination is switched off), resource_gone, not_announced. It never quotes the destination’s URL, the body sent, or what the destination answered; the operator’s delivery log in the UI keeps that text.

An Incident: id, resource {id, key, name}, rule (the Rule identity recorded when it opened), lifecycle, confirmation, condition (ok, problem, no_data), verdict, explanation, closed_reason (recovered, rule_removed), opened_at, confirmed_at, closed_at, effective_at (when its latest recorded transition took effect: opened, confirmed, a verdict change or closed; an acknowledgement does not move it), acknowledgement {by, at, via}, and action (what its Rule told a person to do when it opened, or null). History is [{at, kind, condition, verdict, explanation, evidence}], kinds opened, confirmed, verdict_changed, closed. It is kept after the Rule is edited or removed. evidence is what that transition recorded from the Rule’s inputs when it was decided (below), or null: an administrative closure records none.

Webhooks

A plain webhook destination receives the attachments a Slack destination does, plus an incident object (Slack destinations never get it):

"incident": {
  "id": "p5rO8cFSTi1T", "rule": "access_loss", "verdict": "problem",
  "action": "Investigate the access path.", "transition": "confirmed", "at": "2026-10-05T12:11:00Z",
  "resource": {"key": "srv-0042", "name": "srv-0042", "template": "service-triage@1",
               "fields": {"provider": "alpha", "load_threshold": 0.4}, "fields_omitted": 0},
  "evidence": {"inputs": [
      {"signal": "probe_a", "status": "problem", "at": "2026-10-05T12:10:58Z"},
      {"signal": "load_ratio", "status": "ok", "value": 0.12, "at": "2026-10-05T12:10:59Z"},
      {"signal": "service_health", "unresolved": "the sender reported no_data"}],
    "omitted": 0, "truncated": false},
  "evidence_note": "Recorded when this transition was decided: one reading per declared Rule input. It is not every contributing Observation, and not current context."
}

Archive and filters

Archiving retires a Resource that left the inventory. It is not a deletion: the Resource keeps its id, key, Template revision, metadata and history, and stays readable by id or key. It leaves the active list, its open Incidents close with closed_reason resource_archived (never a recovery, never announced as one), and it takes no more Observations (422 on resource, on API v3 and on the Observation address), edits, maintenance or checks (409). Its key stays reserved: no new Resource can take it. There is no restore. A write already holding the Resource when the archive arrives commits first; anything later is refused, and a worker’s report for it is dropped — no Observation is stored after the archive.

Resource and Incident lists take a bounded filter: template (a Template key, any revision) and up to five meta.<field>=<value> equalities on that Template’s single-valued fields (string, enum, url, integer, number, duration, boolean). The value is read as the field’s type, so meta.load_threshold=0.40 matches 0.4. A field filter without template, an unknown field, a list field or a value the field cannot hold is 422. An Incident filter may match at most 5000 Resources. Every list stays inside the Workspace the key reaches, and pages by a stable cursor.

Pushed-data Templates

A Workspace editor publishes a Template for evidence its own systems push: no URL, no Locations, no managed worker. A revision never changes; publish the next version to change it. Resources keep the revision they were created from.

{"key": "service-triage", "version": 1, "name": "…", "summary": "…",
 "fields":  [{"key": "load_threshold", "label": "…", "type": "number", "default": 0.5, "min": 0}],
 "signals": [{"key": "probe_a", "kind": "heartbeat", "every_seconds": 300}, …],
 "rules":   [{"key": "access_loss", "action": "Investigate the access path.",
              "wait": {"confirm_after": 600, "recover_after": 600},
              "decision": {"all": [
                {"signal": "probe_a", "field": "status", "operator": "eq", "operand": "problem"},
                {"signal": "load_ratio", "field": "value", "operator": "lt",
                 "operand": {"meta": "load_threshold"}}]}}]}

From a key to an Incident

API=http://127.0.0.1:8080/api/v3
KEY=...   # User → API keys
H="Authorization: Bearer $KEY"

WS=$(curl -s -H "$H" $API/workspaces | jq -r '.result[0].id')
LOC=$(curl -s -H "$H" $API/locations | jq -r '.result[0].id')

curl -s -H "$H" -X POST $API/workspaces/$WS/resources -d @- <<EOF | jq '.result | {id, key, signals, rules}'
{"template": "website-check", "key": "checkout-api", "name": "Checkout API",
 "meta": {"url": "https://checkout.example.com/health", "locations": ["$LOC"],
          "interval_value": 5, "interval_unit": "MINUTE",
          "failure_mode": "at_least_one", "confirm_after": 0, "recover_after": 0}}
EOF

SIGNAL=$(curl -s -H "$H" $API/workspaces/$WS/resources/checkout-api | jq -r '.result.signals[0].key')
curl -s -H "$H" -X POST $API/workspaces/$WS/resources/checkout-api/observations \
  -d "{\"signal\": \"$SIGNAL\", \"state\": \"problem\", \"labels\": {\"status\": \"503\"}}"

curl -s -H "$H" $API/workspaces/$WS/resources/checkout-api | jq '.result.rules[0]'
INCIDENT=$(curl -s -H "$H" $API/workspaces/$WS/resources/checkout-api | jq -r '.result.rules[0].open_incident')
curl -s -H "$H" $API/workspaces/$WS/incidents/$INCIDENT | jq '.result | {lifecycle, confirmation, condition, history}'

A fleet from one Template

Create a Resource per host from the same Template, then read every open Incident:

for n in $(seq -w 1 50); do
  curl -s -H "$H" -X POST $API/workspaces/$WS/resources -d @- <<EOF >/dev/null
{"template": "website-check", "key": "shop-$n", "name": "shop-$n.example.com",
 "meta": {"url": "https://shop-$n.example.com/health", "locations": ["$LOC"],
          "interval_value": 1, "interval_unit": "MINUTE",
          "failure_mode": "majority", "confirm_after": 120, "recover_after": 120}}
EOF
done
curl -s -H "$H" "$API/workspaces/$WS/incidents?lifecycle=open&limit=200" | jq '.result[] | {resource: .resource.key, explanation}'

A sender without a person’s key can bring a fleet into being from the Observation address instead: its first report names the Template (template_id and meta) and creates the Resource. See the Observation address in Settings → Observation endpoints.