Reference › REST API

REST API

The API the dashboard is built on. Base /api, always HTTP 200, envelope responses.

Base URL: http://<host>:2517/api — the API is served under /api on the web UI port.

Conventions

Two versions

v2 is the current API and uses the product’s language: locations, website monitoring, incidents. It is self-contained — a v2 client never needs a v1 route.

v1 is frozen and still supported. Every v1 route, field and kind is unchanged, so existing clients keep working with no modification. v1 says regions and rules where v2 says locations and website monitors; the underlying objects are the same.

New integrations should use v2.

Auth

Every endpoint needs a Bearer API key — a token minted in the dashboard (User → API Keys). That includes GET /version: a request with no Authorization header answers Missing Authorization header (code 1001), never a version.

curl -H "Authorization: Bearer $UPTIMER_API_KEY" \
  "http://localhost:2517/api/v2/monitoring/websites?workspace_id=<uid>"

The token is shown once, on the screen that creates it, and is valid for 180 days — see API keys for rotation. A key can only reach the workspaces its owner is a member of; anything else answers Access denied.

Endpoints (v2)

Every row links to that method’s own section.

MethodPathPurpose
GET/versionServer version (string). Shared by v1 and v2.
GET/v2/workspacesYour workspaces, each with your role.
GET/v2/locationsLocations + active worker counts.
GET/v2/incidentsOpen incidents, newest trouble first.
GET/v2/monitoring/websitesList website monitors.
POST/v2/monitoring/websitesCreate a website monitor.
GET/v2/monitoring/websites/{id}Get one.
POST/v2/monitoring/websites/{id}Update one (there is no PUT).
DELETE/v2/monitoring/websites/{id}Delete one.
GET/v2/subjectsThe workspace’s Custom subjects.
POST/v2/subjectsCreate one empty Custom subject.
GET/v2/subjects/{subject}Get one by its slug.
GET/v2/subjects/{subject}/signalsThe subject’s signals.
POST/v2/subjects/{subject}/signalsAdd one custom signal.
GET/v2/subjects/{subject}/signals/{signal}Get one.
POST/v2/subjects/{subject}/signals/{signal}Rename it and replace its meta.
DELETE/v2/subjects/{subject}/signals/{signal}Delete it and its observations.
GET/v2/subjects/{subject}/rulesThe subject’s incident rules.
POST/v2/subjects/{subject}/rulesAuthor one rule.
GET/v2/subjects/{subject}/rules/{rule}Get one.
POST/v2/subjects/{subject}/rules/{rule}Replace its name and policy.
DELETE/v2/subjects/{subject}/rules/{rule}Delete it.
POST/v2/subjects/{subject}/signals/{signal}/observationsReport one observation to a custom signal.
GET/v2/subjects/{subject}/incidentsThat subject’s open incidents, with their ids.
GET/v2/subjects/{subject}/maintenanceThe running maintenance window, or null.
POST/v2/subjects/{subject}/maintenanceStart one, ending when you say.
POST/v2/subjects/{subject}/maintenance/ends_atMove the end of the running one.
DELETE/v2/subjects/{subject}/maintenanceEnd it early.
POST/v2/subjects/{subject}/incidents/{incident}/acknowledgeSay you have seen one open Custom incident.
GET/v2/subjects/{subject}/deliveryWhich destinations this Custom subject tells, and about what.
POST/v2/subjects/{subject}/deliveryReplace that table.
DELETE/v2/subjects/{subject}/deliveryClear it — back to the workspace default.
GET/v2/monitoring/websites/{id}/deliveryThe same table for a Website monitor.
POST/v2/monitoring/websites/{id}/deliveryReplace it.
DELETE/v2/monitoring/websites/{id}/deliveryClear it.
GET/v2/notifications/destinationsThe workspace’s destinations, by name.
POST/v2/notifications/destinationsCreate one, enabled.
GET/v2/notifications/destinations/{id}Get one.
POST/v2/notifications/destinations/{id}Update name, channel, URL, default, transformation.
DELETE/v2/notifications/destinations/{id}Delete it, and every subject selection naming it.
POST/v2/notifications/destinations/{id}/enabledSwitch it on or off.
POST/v2/notifications/destinations/{id}/defaultMake it the workspace default.
POST/v2/notifications/destinations/{id}/testSend one real test message.
GET/v2/notifications/transformationsThe workspace’s payload templates.
POST/v2/notifications/transformationsStore one, if it renders every sample.
GET/v2/notifications/transformations/samplesThe three messages a template is judged against.
POST/v2/notifications/transformations/previewRender a template against all three, storing nothing.
GET/v2/notifications/transformations/{id}Get one.
POST/v2/notifications/transformations/{id}Replace its name and template.
DELETE/v2/notifications/transformations/{id}Delete it; destinations fall back to the built-in body.
GET/v2/notifications/deliveriesThe delivery log, filtered.

The API is split by subject kind

There are two kinds of subject, and each has one API of its own.

Neither one serves the other’s subjects. GET /v2/subjects lists Custom subjects only, and a website subject answers Website subjects are managed elsewhere (code 2004) on every /v2/subjects route — read, signal, rule and observation alike. In the other direction, a subject that is being maintained by hand is no longer the website API’s to serve: it drops out of the website listing, and reading, updating or deleting its monitor answers Custom subjects are managed elsewhere (code 2004), naming the /v2/subjects path to use instead. Ordinary website monitors are unaffected.

A subject becomes Custom the moment it is given its first custom signal or authored rule, and never goes back. Subjects created before 1.6.0 are classified once, on upgrade: one that already carried a hand-made signal or rule is Custom, and everything else is Website.

There is no PUT, no update and no DELETE for a subject itself: deleting one takes its whole history with it, so it is a dashboard action rather than a script’s.

Endpoints (v1, frozen)

MethodPathPurpose
GET/v1/workspacesYour workspaces, each with your role.
GET/v1/rulesList rules in a workspace.
POST/v1/rulesCreate a rule.
GET/v1/rules/{uid}Get one rule.
POST/v1/rules/{uid}Update a rule.
DELETE/v1/rules/{uid}Delete a rule.
POST/v1/rules/{uid}/incidents/{incident}/acknowledgeSay you have seen one open Website incident.
GET/v1/regionsList regions + active worker counts.

Shared payloads

These schemas are referenced by more than one method, so they are written once here.

Website monitor object (v2)

{
  "name": "home",
  "interval": 60,
  "workspace_id": "<uid>",
  "request":  { "url": "https://example.com", "method": "GET", "content_type": "application/json", "data": "" },
  "response": { "statuses": [200], "body": { "content": "" } },
  "locations": ["local"],
  "agreement": "majority"
}

The response is the stored monitor: the same fields plus id, and a kind on every object (website_monitor, website_monitor_request, website_monitor_response, website_monitor_response_body). kind is the server telling you what an object is — you never send it back, and it is ignored if you do. locations always comes back as a list, never null.

Incident object (v2)

{
  "id": "v8dLj9O1tzs",
  "monitor_id": "<uid>",
  "monitor_name": "home",
  "status": "problem",
  "trouble_since": "2026-08-20T18:21:37Z",
  "confirmed_at": "2026-08-20T18:24:37Z",
  "well_since": null,
  "locations": { "failing": ["local"], "unknown": [], "ok": [] },
  "kind": "incident"
}

status uses the same words the dashboard shows, so the API and the screen cannot disagree:

statusmeaning
problemconfirmed; notifications have gone out
pendingfailing, but inside the confirm hold — nobody has been notified yet
recoveringreporting ok again while the incident is still open
no_datanothing usable arrived
okhealthy

locations is the evidence the verdict came from. A location that has never reported stays in unknown and still counts toward the agreement — that is a real state, not a missing one.

Subject incident object (v2)

One open incident of a Custom subject, returned by list a custom subject’s open incidents.

{
  "id": "xeOkfGadru8",
  "subject_id": "payments-worker",
  "rule_id": "export-health",
  "rule_name": "Export health",
  "status": "problem",
  "trouble_since": "2026-09-12T10:47:45Z",
  "confirmed_at": "2026-09-12T10:47:45Z",
  "well_since": null,
  "acknowledged": true,
  "acknowledged_at": "2026-09-12T11:47:45Z",
  "acknowledged_by": "ops",
  "kind": "subject_incident"
}

It is not the incident object above: that one describes a website monitor’s incident and carries monitor_id, monitor_name and the locations the probe ran from. A custom incident has none of those, so instead of inventing them this object names the rule that opened it — rule_id is the rule’s slug, rule_name the name you gave it.

Incident acknowledgement object

The answer to both acknowledge methods. One shape for both families: what differs is only how the incident was addressed.

{
  "incident_id": "xeOkfGadru8",
  "monitor_id": "<uid>",
  "status": "problem",
  "acknowledged": true,
  "acknowledged_at": "2026-09-12T11:30:57Z",
  "acknowledged_by": "ops",
  "recorded": true,
  "trouble_since": "2026-09-12T10:30:57Z",
  "confirmed_at": "2026-09-12T10:30:57Z",
  "well_since": null,
  "closed_at": null,
  "kind": "incident_acknowledgement"
}

Subject object (v2)

One thing a workspace watches, returned by the subject methods.

{
  "id": "payments-worker",
  "name": "Payments worker",
  "subject_kind": "custom",
  "workspace_id": "<uid>",
  "signal_count": 1,
  "rule_count": 1,
  "kind": "subject"
}
FieldMeaning
idThe subject’s slug — what the API addresses it by, and the first half of the observation route. There is no database id in the payload, and renaming the subject does not move it.
subject_kindwebsite or custom. How the subject is configured, and therefore what may be done to it.
signal_count, rule_countHow much is under the subject. There is no signals or rules collection, so these are how you see that a new Custom subject really is empty.
kindAlways "subject".

kind and subject_kind are separate on purpose: kind says what the object is, so a client switching on it keeps working when a third subject kind arrives; subject_kind says how this particular one is configured. On these routes it is always custom — a website subject is served by the website API and refused here.

Signal object (v2)

One signal of a Custom subject, returned by the signal methods.

{
  "id": "worker-pulse",
  "name": "Worker pulse",
  "signal_kind": "custom_heartbeat",
  "subject_id": "payments-worker",
  "workspace_id": "<uid>",
  "meta": {},
  "kind": "signal"
}
FieldMeaning
idThe signal’s slug — the address a sender posts to, and what a rule input cites. A rename never moves it.
signal_kindcustom_heartbeat or custom_event. Fixed at creation.
metaAny JSON object, stored and returned untouched. Uptimer never reads a key out of it.

Rule object (v2)

One operator-authored incident rule of a Custom subject, returned by the rule methods. It is deliberately not the v1 rule object, which describes a website probe.

{
  "id": "export-health",
  "name": "Export health",
  "subject_id": "payments-worker",
  "workspace_id": "<uid>",
  "policy_version": 1,
  "document": {
    "inputs": [
      { "signal": "worker-pulse", "mode": "status", "no_data_after": "5m0s" },
      { "signal": "queue-depth", "mode": "latest_value", "compare": ">", "threshold": 1000 },
      { "from": "queue-health" }
    ],
    "decision": { "state": "down", "need": "any" },
    "wait": { "confirm_after": "2m0s", "close_after": "2m0s" }
  },
  "kind": "subject_rule"
}
FieldMeaning
idThe rule’s slug — what another rule cites with from, and what the nested routes address. A rename never moves it.
policy_versionIncremented on every saved policy. The stored document is kept per version, so a past verdict can be read back against the policy that produced it.
documentThe policy itself, below.

document.inputs enumerates what the rule reads. Each input sets exactly one of:

Cross-subject inputs are not possible — a subject is the boundary — and a signal or rule an input cites cannot be deleted while it does.

A signal input takes:

FieldMeaning
modestatus (the latest selected observation reports problem) or latest_value (its numeric value is compared). Required.
compare, threshold< or > and one number. latest_value only. An observation with no number is unknown rather than false.
matchAND-ed equality on the observation’s labels. "*" means the key must be present with any value; no entries selects the whole signal.
no_data_afterA duration string ("5m", "2m0s") — how long this input may stay silent before it counts as unknown. Omit or 0s to derive it from the interval. Meaningless on an event signal and on a from input.

document.decision is {"state": "down", "need": …}state is always down in this release, and need is any, majority, all or at_least with at_least: N.

document.wait is confirm_after (how long a problem must last before the incident is confirmed and anyone is alerted — the incident opens on the first bad tick regardless) and close_after (how much continuous recovery closes it). Both are duration strings and both default to "2m0s".

Durations are strings, not numbers of seconds, so a stored policy reads the way an operator would write it.

Observation object (v2)

What the server stored, returned by report an observation.

{
  "subject_id": "payments-worker",
  "signal_id": "worker-pulse",
  "observed_at": "2026-09-01T12:00:00Z",
  "received_at": "2026-09-01T12:00:01Z",
  "status": "ok",
  "value": 1.5,
  "error": "",
  "labels": { "instance": "worker-3" },
  "accepted": true,
  "reject_reason": "accepted",
  "kind": "observation"
}
FieldMeaning
subject_id, signal_idThe two slugs you addressed, echoed back.
observed_atWhen you say you observed it. Omitted on the request, the server stamps now.
received_atWhen the server stored it. The pair explains a late or skewed report.
statusok or problem, as sent.
valueThe optional number, or null.
labelsYour labels, as sent.
acceptedWhether evaluation may use this row.
reject_reasonaccepted, or why not: clock_skew, late, out_of_order, out_of_retention.

accepted reports acceptance, not health: it says Uptimer stored the observation and may evaluate it, not that anything is fine or broken. Whether a problem raises an incident is decided by a rule, and a signal no rule reads raises nothing at all.

Rule object (v1, frozen)

{
  "name": "home",
  "interval": 60,
  "workspace_id": "<uid>",
  "request":  { "url": "https://example.com", "method": "GET", "content_type": "application/json", "data": "" },
  "response": { "statuses": [200], "body": { "content": "" } },
  "regions":  ["local"]
}

Same field rules as the website monitor object, with regions in place of locations. v1 has no agreement field: a rule created through v1 uses the default, majority.

The response adds id and carries the v1 kinds — rule, rule_request, rule_response, rule_response_body.

v2 methods

Get the server version

GET /version

No parameters, no body. result is a plain JSON string:

{ "result": "1.6.0", "error": null, "meta": null }

Not versioned — it is shared by v1 and v2, so a client can read it before it knows which API versions the server has. It still needs a Bearer token like every other endpoint. It is the one route registered outside the API-key lookup: the token is validated, but the key record behind it is not loaded.

List workspaces (v2)

GET /v2/workspaces

No parameters, no body. Returns every workspace the key’s owner is a member of, each with that member’s role:

[{ "id": "<uid>", "name": "Default", "role": "owner", "kind": "workspace" }]

role is owner, editor or viewer. This is a path alias over the v1 handler — its payload is already in v2’s vocabulary — so a v2 client never has to call /v1.

List locations

GET /v2/locations

No parameters, no body. Returns every location on the instance with how many workers are currently serving it:

[{ "id": "<uid>", "name": "eu-west", "active_workers_count": 0, "kind": "location" }]

Use name when assigning locations to a monitor — the create and update payloads match by name, not by id. active_workers_count is 0 for a location no worker is serving; a monitor assigned only to such a location has nothing reporting for it.

List open incidents

GET /v2/incidents?workspace_id=<uid>

ParameterRequiredMeaning
workspace_idyesThe workspace to read. Missing → Missing workspace ID (code 2004).
monitor_idnoNarrow the answer to one website monitor.

Returns a list of incident objects, newest trouble_since first — it answers “what is wrong now”.

Open incidents only. Closed incidents are history and live on the subject timeline in the dashboard; there is no incident-history endpoint in this release. A monitor that has never been evaluated has nothing open and simply does not appear, and a workspace with nothing wrong answers [].

A key whose owner is not a member of that workspace gets Access denied (code 2005).

List website monitors

GET /v2/monitoring/websites?workspace_id=<uid>

ParameterRequiredMeaning
workspace_idyesThe workspace to read. Missing → Missing workspace ID (code 2004).

Returns a list of stored website monitor objects, each with its id, its locations and its agreement. A non-member gets Access denied (code 2005).

Create a website monitor

POST /v2/monitoring/websites

Body: a website monitor object without id.

Returns the stored monitor, so the id to address it with comes back in the response.

Saving a website monitor also creates its monitoring subject, its built-in HTTP signal and its Reachability rule — you do not create those separately, and they are in place before the response is written, so a monitor created here behaves exactly like one created in the form.

An unknown location name answers invalid locations / Unknown location: "…" (code 2001), and nothing is written.

Get a website monitor

GET /v2/monitoring/websites/{id}

{id} is the monitor’s id, as returned by create or list. No body.

Returns one stored website monitor object. An id that does not exist, or one in a workspace the key cannot reach, answers Website monitor not found (code 2002) — the two are deliberately indistinguishable.

Update a website monitor

POST /v2/monitoring/websites/{id} — there is no PUT.

Body: a website monitor object without id. The update is a full replacement, not a patch: send the whole configuration, because anything you leave out is not kept.

Three fields behave differently from create:

Returns the stored monitor. An id that does not exist answers Website monitor not found (code 2002).

Delete a website monitor

DELETE /v2/monitoring/websites/{id}

No body. Requires an editor or owner role in the monitor’s workspace; a viewer gets Access denied (code 2005).

{ "message": "Website monitor deleted successfully", "monitor_id": "<uid>" }

This removes the monitor and everything under it — its monitoring subject, its signal, its rule and their history. Deleting an id that is already gone answers Website monitor not found (code 2002).

List custom subjects

GET /v2/subjects?workspace_id=<uid>

ParameterRequiredMeaning
workspace_idyesThe workspace to read. Missing → Missing workspace ID (code 2004).

Returns the workspace’s Custom subject objects, each with its signal_count and rule_count. Website subjects are not here — they are the website API’s. A non-member gets Access denied (code 2005).

Create a custom subject

POST /v2/subjects

{ "workspace_id": "<uid>", "name": "Payments worker" }
FieldRequiredMeaning
workspace_idyesWhere the subject is created.
nameyesMust contain at least one letter or digit; it produces the slug.
subject_kindnoMay only say "custom". It exists so a client that sends the field is answered rather than surprised.

Returns the stored subject object. The subject arrives emptysignal_count and rule_count are 0, and it has no HTTP probe. Give it a signal, then a rule that reads it, then report observations.

A colliding name is not refused: two subjects may legitimately be called the same thing, so the slug is disambiguated (payments-worker-2). The address is not the name, and a later rename never moves it.

Unknown fields are refused rather than dropped, so a body carrying url or interval answers Invalid JSON (code 2006) instead of quietly creating something that probes nothing.

Website monitoring is not created here. subject_kind: "website" answers Website subjects are created elsewhere (code 2004), pointing at POST /v1/rules and its /v2/monitoring/websites alias.

Creating requires the workspace editor role; a viewer gets Access denied (code 2005).

Get a custom subject

GET /v2/subjects/{subject_slug}

{subject_slug} is the subject’s id. Returns one subject object.

ParameterRequiredMeaning
workspace_idnoSettles an ambiguity rather than being required.

A subject slug is unique per workspace, not globally, so without workspace_id the server searches the workspaces you belong to. Nothing found answers Subject not found (code 2002) — the same answer a subject you cannot see gives. The same slug in two of your workspaces answers Ambiguous subject (code 2004), naming them, so you can add the parameter.

A website subject answers Website subjects are managed elsewhere (code 2004). Reading a custom one needs only the viewer role: it is what you can already see in the dashboard.

Custom signals

Every route below is nested under a Custom subject, and every one of them refuses a website subject with Website subjects are managed elsewhere (code 2004). Reads need the viewer role; writes need editor.

List signals

GET /v2/subjects/{subject_slug}/signals

Returns the subject’s signal objects. A subject you just created has none.

Create a signal

POST /v2/subjects/{subject_slug}/signals

{ "name": "Worker pulse", "kind": "custom_heartbeat", "meta": { "team": "payments" } }
FieldRequiredMeaning
nameyesMust contain at least one letter or digit; it produces the slug.
kindyescustom_heartbeat or custom_event (heartbeat and event are accepted spellings). Fixed once created.
metanoAny JSON object, stored and returned untouched.

Returns the stored signal object. Choosing between heartbeat and event is choosing what your silence means — see Signals & rules.

AnswerWhen
Invalid signal kind (code 2001)kind is missing or is not one of the four accepted words. A platform HTTP signal cannot be authored.
Invalid name (code 2001)The name has no letter or digit.
Signal name taken (code 2001)Another signal on this subject already has that slug.
Invalid meta (code 2001)meta is not a JSON object.

Get a signal

GET /v2/subjects/{subject_slug}/signals/{signal_slug}

Returns one signal object, or Signal not found (code 2002).

Update a signal

POST /v2/subjects/{subject_slug}/signals/{signal_slug} — there is no PUT.

{ "name": "Worker heartbeat", "meta": {} }

Renames the signal and replaces its meta. kind and the slug are immutable: senders are already posting to that address, so a rename never moves it.

A built-in signal — the platform HTTP one a website monitor maintains — answers Signal is managed by Website monitoring (code 2003).

Delete a signal

DELETE /v2/subjects/{subject_slug}/signals/{signal_slug}

{ "message": "Signal deleted successfully", "signal_id": "worker-pulse", "subject_id": "payments-worker" }

This removes the signal and its observations. A signal a rule reads answers Signal is still used by a rule (code 2003): retarget or remove those rules first. Uptimer never unlinks a rule on its own, because that would quietly change what the rule watches in order to complete an unrelated delete.

Custom rules

The same nesting, the same roles, the same cross-kind refusal as the signal routes above.

List subject rules

GET /v2/subjects/{subject_slug}/rules

Returns the subject’s rule objects, each with its policy document.

Author a rule

POST /v2/subjects/{subject_slug}/rules

{
  "name": "Export health",
  "document": {
    "inputs": [{ "signal": "worker-pulse", "mode": "status", "no_data_after": "5m" }],
    "decision": { "state": "down", "need": "any" },
    "wait": { "confirm_after": "2m", "close_after": "2m" }
  }
}

Returns the stored rule object at policy_version: 1. Every input must cite a signal or a rule of this subject — add the signals first.

AnswerWhen
Invalid name (code 2001)The name has no letter or digit.
Rule name taken (code 2001)Another rule on this subject already has that slug.
Unknown input (code 2001)An input cites a signal or rule this subject does not have.
Invalid input mode (code 2001)Neither or both of status and latest_value, or a mode on a rule input.
Invalid rule document (code 2001)The document is otherwise not a valid policy.

Get a subject rule

GET /v2/subjects/{subject_slug}/rules/{rule_slug}

Returns one rule object, or Rule not found (code 2002).

Update a subject rule

POST /v2/subjects/{subject_slug}/rules/{rule_slug} — there is no PUT.

Body: the same shape as author. The policy is a full replacement, not a patch, and a successful save increments policy_version. The rule keeps its identity and its slug, so the incidents and timeline already pointing at it stay attached.

A rule website monitoring created answers Rule is managed by Website monitoring (code 2003): its policy is the check form’s, and a save here would be rewritten on the next check save.

Delete a subject rule

DELETE /v2/subjects/{subject_slug}/rules/{rule_slug}

{ "message": "Rule deleted successfully", "rule_id": "export-health", "subject_id": "payments-worker" }

A rule another rule cites as an input answers Rule is still used as an input (code 2003); a built-in Reachability rule answers Rule is managed by Website monitoring (code 2003) and cannot be deleted at all.

Observations

Report an observation

POST /v2/subjects/{subject_slug}/signals/{signal_slug}/observations

Reports one observation to a custom heartbeat or event signal of a Custom subject. Both slugs are shown on the signal’s page in the dashboard, and by list signals.

FieldRequiredMeaning
statusyesok or problem.
observed_atnoRFC 3339, e.g. 2026-09-01T12:00:00Z. Omit to mean now.
valuenoA number a rule can compare with < or >.
errornoYour own error text, for a problem worth explaining.
labelsnoAn open string map that rules match on.
curl -X POST "$UPTIMER_URL/api/v2/subjects/payments-worker/signals/worker-pulse/observations"   -H "Authorization: Bearer $UPTIMER_TOKEN"   -H "Content-Type: application/json"   -d '{"status":"ok","value":1.5,"labels":{"instance":"worker-3"}}'

Returns the stored observation object.

A stored observation the engine will not evaluate is returned, not refused. A timestamp far in the future comes back with accepted: false and reject_reason: "clock_skew" — it was received and kept. An error means nothing was stored.

Refusals, all of which store nothing:

AnswerWhen
invalid status (code 2001)status missing, or not ok/problem.
invalid observed_at (code 2001)Not an RFC 3339 timestamp.
invalid label (code 2001)An empty label key, or a key/value over its length limit.
Signal not found (code 2002)No signal with that slug on the subject.
Subject not found (code 2002)No subject with that slug in a workspace you belong to — the same answer a subject you cannot see gives.
Signal does not accept posted observations (code 2003)The signal is platform HTTP. Its stream belongs to Uptimer’s own probe, so a posted claim is never mixed in with a measurement.
Website subjects are managed elsewhere (code 2004)The subject is a website check. Its readings are its workers'.

The workspace comes from your token’s membership, not the path: a subject slug is unique per workspace, not globally. If the same slug exists in two of your workspaces the answer is Ambiguous subject (code 2004), naming them — add ?workspace_id=<uid> to choose.

Reporting requires the workspace editor role; a viewer gets Access denied (code 2005).

Retries are safe: an observation is identified by its signal, observed_at and labels, so re-sending the same one replaces it rather than counting twice.

List a custom subject’s open incidents

GET /v2/subjects/{subject}/incidents?workspace_id=<uid>

New in 1.7.0. The open incidents of one Custom subject, with the ids the acknowledge method takes.

ParameterWhereMeaning
subjectpathThe Custom subject’s slug.
workspace_idqueryOnly needed when the same subject slug exists in two of your workspaces.
curl "$UPTIMER_URL/api/v2/subjects/payments-worker/incidents" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"
{ "result": [
    { "id": "v8dLj9O1tzs", "subject_id": "payments-worker", "rule_id": "queue-depth",
      "rule_name": "Queue depth", "status": "problem",
      "trouble_since": "2026-09-12T10:47:45Z", "confirmed_at": "2026-09-12T10:47:45Z",
      "well_since": null, "acknowledged": false, "acknowledged_at": null,
      "acknowledged_by": "", "kind": "subject_incident" },
    { "id": "xeOkfGadru8", "subject_id": "payments-worker", "rule_id": "export-health",
      "rule_name": "Export health", "status": "problem",
      "trouble_since": "2026-09-12T10:47:45Z", "confirmed_at": "2026-09-12T10:47:45Z",
      "well_since": null, "acknowledged": true, "acknowledged_at": "2026-09-12T11:47:45Z",
      "acknowledged_by": "ops", "kind": "subject_incident" } ],
  "error": null, "meta": null }

Returns a list of subject incident objects, newest trouble_since first; ties fall back to the rule slug, so the order is the same on every call.

Open incidents only, and all of them. A subject can have one incident open per rule, and they are all listed — pending, recovering and no-data included, and acknowledged ones too: that somebody is already on one is half of what you ask this for. Nothing open answers []. Closed history is not here; it lives on the subject timeline in the dashboard.

This is a read, so the workspace viewer role is enough — acknowledging one of them is not. A website subject answers Website subjects are managed elsewhere (code 2004) like every other /v2/subjects route: its incidents are listed by GET /v2/incidents.

Maintenance windows

New in 1.7.0. A maintenance window holds back one subject’s problem notifications until a time you choose — for a deploy, a migration, anything that will make it look broken on purpose. Monitoring, evidence, incidents and the timeline are untouched, so the outage still reads afterwards exactly as it happened, and recoveries are never held back.

Four operations, on the subject: read the running window, start one, move its end, end it early. There is no history listing — past windows are not something a client acts on.

Like everything else under /v2/subjects, this is Custom subjects only. A website check is put into maintenance from its page in the dashboard; a website subject answers Website subjects are managed elsewhere (code 2004) here.

Reading takes the workspace viewer role; starting a window, moving its end and cancelling it take editor.

Read the running window

GET /v2/subjects/{subject}/maintenance?workspace_id=<uid>

curl "$UPTIMER_URL/api/v2/subjects/payments-worker/maintenance" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"
{ "result": {
    "subject_id": "payments-worker",
    "started_at": "2026-09-13T09:00:00Z",
    "ends_at": "2026-09-13T11:00:00Z",
    "cancelled_at": null,
    "active": true,
    "muted": ["problem", "no_data"],
    "kind": "maintenance_window" },
  "error": null, "meta": null }

Nothing scheduled answers "result": null, not an error: a script asking “is it safe to deploy?” should not have to catch an exception for the ordinary case.

active is the question you actually have — is this subject silenced right now? — answered by the server rather than left to you comparing three timestamps against your own clock. The three states are told apart by the fields: active true is running, cancelled_at set is ended early, and neither is a window that simply ran out. muted says what waits, and a recovery is never in it.

Start one

POST /v2/subjects/{subject}/maintenance

FieldRequiredMeaning
ends_atyesRFC 3339, and it carries its own zone: 2026-09-13T18:00:00Z or 2026-09-13T20:00:00+02:00.

The window starts immediately; there is no future start and no recurring schedule.

curl -X POST "$UPTIMER_URL/api/v2/subjects/payments-worker/maintenance" \
  -H "Authorization: Bearer $UPTIMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ends_at":"2026-09-13T11:00:00Z"}'

Returns the window. Refusals, none of which change anything:

AnswerWhen
invalid ends_at (code 2001)Missing, unparsable, or a time that has already passed.
Already in maintenance (code 2004)A window is running. Cancel it before starting another.
Website subjects are managed elsewhere (code 2004)The subject is a website check.
Subject not found (code 2002)No such subject in a workspace you belong to.
Access denied (code 2005)The key’s owner is a viewer, or not a member.

Move the end

POST /v2/subjects/{subject}/maintenance/ends_at

New in 1.7.0. Moves the end of the window that is already running. It POSTs to its own path because POSTing the collection already means “start one”.

FieldRequiredMeaning
ends_atyesThe new end, RFC 3339, as for starting one.
curl -X POST "$UPTIMER_URL/api/v2/subjects/payments-worker/maintenance/ends_at" \
  -H "Authorization: Bearer $UPTIMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ends_at":"2026-09-13T15:00:00Z"}'

It is an update, not a cancel and a new window. The window keeps its identity and its started_at, so “since when have we been silencing this?” keeps one answer and nothing that reads it sees the subject briefly leave maintenance. Nothing is notified: moving an end time is a correction to a plan, not an event.

Returns the window with its new ends_at. Refusals, none of which change anything:

AnswerWhen
invalid ends_at (code 2001)Missing, unparsable, or already passed. To stop the window now, cancel it instead.
No maintenance window (code 2002)Nothing is running on this subject, so there is no end to move.
Website subjects are managed elsewhere (code 2004)The subject is a website check.
Access denied (code 2005)The key’s owner is a viewer, or not a member.

End it early

DELETE /v2/subjects/{subject}/maintenance

curl -X DELETE "$UPTIMER_URL/api/v2/subjects/payments-worker/maintenance" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"

Returns the window as it was recorded, with cancelled_at set and active false. Notifications are normal again immediately. A subject with no window running answers No maintenance window (code 2002) — “it was already over” is worth knowing rather than reported as success.

Expiry is silent. Nothing is sent when a window ends and nothing is replayed: a problem that is still there afterwards notifies under the normal rules the next time it would have.

Acknowledging an incident

New in 1.7.0. Acknowledging says a person has seen an open incident. It changes nothing the engine decided — the verdict, the evidence and the close hold all carry on — and it is recorded once, with who and when, on the incident’s timeline. Its one effect on alerting is that the four-hour reminders for that incident stop. The dashboard does the same thing from the subject’s page; see Acknowledging an incident.

Each kind of monitoring acknowledges through its own API, like everything else (the split):

MonitoringMethod
WebsitePOST /v1/rules/{uid}/incidents/{incident}/acknowledge
CustomPOST /v2/subjects/{subject}/incidents/{incident}/acknowledge

Both take no body: the person recorded is the owner of the API key you called with, and the time is the time of the call. A body is refused rather than ignored (This request takes no body, code 2001) — silently filing an acknowledgement under somebody else’s name would be worse than any error.

Both name one exact incident. There is no “the current incident”: a subject can have several open at once, and an id that belongs to another monitor, another subject, another workspace or the other kind of monitoring is simply Incident not found (code 2002) where you asked for it.

Where the incident id comes from — one list per kind, as everywhere else. For a website monitor, GET /v2/incidents has returned each open incident’s id since 1.5.0, and remains the Website discovery path. For a custom subject, GET /v2/subjects/{subject}/incidents does the same for that subject. Both flows are API-only:

# Website: find it, then acknowledge it.
INCIDENT_ID=$(curl -s "$UPTIMER_URL/api/v2/incidents?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $UPTIMER_TOKEN" | jq -r '.result[0].id')
MONITOR_UID=$(curl -s "$UPTIMER_URL/api/v2/incidents?workspace_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $UPTIMER_TOKEN" | jq -r '.result[0].monitor_id')
curl -X POST "$UPTIMER_URL/api/v1/rules/$MONITOR_UID/incidents/$INCIDENT_ID/acknowledge" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"

# Custom: the same two steps, under the subject.
INCIDENT_ID=$(curl -s "$UPTIMER_URL/api/v2/subjects/payments-worker/incidents" \
  -H "Authorization: Bearer $UPTIMER_TOKEN" | jq -r '.result[0].id')
curl -X POST "$UPTIMER_URL/api/v2/subjects/payments-worker/incidents/$INCIDENT_ID/acknowledge" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"

Pick the incident you mean rather than .result[0] when a subject has several open — that is what rule_id and rule_name are in the listing for.

Both require the workspace editor role: acknowledging writes a claim about a person, so a viewer gets Access denied (code 2005) — they can still read that an incident was acknowledged.

Acknowledge a website incident

POST /v1/rules/{uid}/incidents/{incident}/acknowledge

ParameterWhereMeaning
uidpathThe website monitor’s uid — the same id GET /v1/rules returns.
incidentpathThe incident’s opaque id.
curl -X POST "$UPTIMER_URL/api/v1/rules/$MONITOR_UID/incidents/$INCIDENT_ID/acknowledge" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"
{ "result": {
    "incident_id": "xeOkfGadru8",
    "monitor_id": "<uid>",
    "status": "problem",
    "acknowledged": true,
    "acknowledged_at": "2026-09-12T11:30:57Z",
    "acknowledged_by": "ops",
    "recorded": true,
    "trouble_since": "2026-09-12T10:30:57Z",
    "confirmed_at": "2026-09-12T10:30:57Z",
    "well_since": null,
    "closed_at": null,
    "kind": "incident_acknowledgement" },
  "error": null, "meta": null }

Returns the incident acknowledgement object.

Acknowledge a custom incident

POST /v2/subjects/{subject}/incidents/{incident}/acknowledge

ParameterWhereMeaning
subjectpathThe Custom subject’s slug.
incidentpathThe incident’s opaque id.
workspace_idqueryOnly needed when the same subject slug exists in two of your workspaces.
curl -X POST "$UPTIMER_URL/api/v2/subjects/payments-worker/incidents/$INCIDENT_ID/acknowledge" \
  -H "Authorization: Bearer $UPTIMER_TOKEN"
{ "result": {
    "incident_id": "v8dLj9O1tzs",
    "subject_id": "payments-worker",
    "rule_id": "export-health",
    "status": "problem",
    "acknowledged": true,
    "acknowledged_at": "2026-09-12T11:30:57Z",
    "acknowledged_by": "ops",
    "recorded": true,
    "trouble_since": "2026-09-12T10:30:57Z",
    "confirmed_at": "2026-09-12T10:30:57Z",
    "well_since": null,
    "closed_at": null,
    "kind": "incident_acknowledgement" },
  "error": null, "meta": null }

rule_id is the rule the incident belongs to — useful on a subject with several rules, where acknowledging one incident says nothing about the others.

Repeats, closed incidents and refusals

A repeat is safe and keeps the first person. Acknowledging again succeeds, adds no second history entry, and answers recorded: false with the original acknowledged_at and acknowledged_by. A retried request is therefore not a second claim.

A closed incident cannot be newly acknowledged: Incident is closed (code 2004). One that was acknowledged while it was open keeps that acknowledgement after closing, and acknowledging it again answers recorded: false rather than an error — the look did happen.

AnswerWhen
Incident not found (code 2002)No such incident under the monitor or subject you named — including one that belongs to another monitor, another subject, another workspace, or the other kind of monitoring. A malformed id answers the same way.
Incident is closed (code 2004)The incident closed before the call arrived and had not been acknowledged. Anything open now is a different incident.
This request takes no body (code 2001)Something was sent in the body.
Custom subjects are managed elsewhere (code 2004)The v1 path named a monitor whose subject is maintained by hand — acknowledge it on /v2/subjects.
Website subjects are managed elsewhere (code 2004)The v2 path named a website subject — acknowledge it on /v1/rules.
Access denied (code 2005)The key’s owner is a viewer, or not a member of that workspace.

Notifications

New in 1.8.0. Everything the Destinations, Transformations, Alert delivery and Delivery screens do, over the API and against the same model: a destination created here is the row the dashboard lists, and a template stored here is judged by the rule the editor enforces.

Destination object

{
  "id": 1,
  "name": "Acme · #incidents",
  "destination_type": "slack",
  "channel": "incidents",
  "url": "https://hooks.slack.com/services/T000/B000/xxxx",
  "enabled": true,
  "default": true,
  "transformation_id": null,
  "workspace_id": "<uid>",
  "created_at": "2026-09-19T08:33:48Z",
  "updated_at": "2026-09-19T08:33:48Z",
  "kind": "notification_destination"
}

List destinations

GET /v2/notifications/destinations?workspace_id=<uid> — by name.

curl -H "Authorization: Bearer $UPTIMER_API_KEY" \
  "http://localhost:2517/api/v2/notifications/destinations?workspace_id=$WS"

Create a destination

POST /v2/notifications/destinations?workspace_id=<uid>

{ "name": "Acme · #incidents", "destination_type": "slack",
  "channel": "#incidents", "url": "https://hooks.slack.com/services/…",
  "default": false, "transformation_id": null }

The first destination in a workspace becomes the default whether or not it asks, because a workspace whose only destination is not the default notifies nobody.

AnswerWhen
Invalid name (code 2001)The name is empty.
Destination name taken (code 2001)Another destination in this workspace already has that name.
Invalid url (code 2001)Not an http:// or https:// address.
Invalid destination type (code 2001)Not slack or webhook.
Transformation not found (code 2002)transformation_id is not a transformation in this workspace.

Get a destination

GET /v2/notifications/destinations/{id}?workspace_id=<uid>

Update a destination

POST /v2/notifications/destinations/{id}?workspace_id=<uid> — name, channel, URL, default and transformation. The type is not editable.

Switch one on or off

POST /v2/notifications/destinations/{id}/enabled?workspace_id=<uid>

{ "enabled": false }

A disabled destination keeps its name, its address and every subject selection pointing at it, and sends nothing until it is switched back on.

Make one the workspace default

POST /v2/notifications/destinations/{id}/default?workspace_id=<uid> — no body. Moving the default clears it from whichever destination held it.

A disabled destination is refused: Destination is disabled (code 2004). A fallback that cannot receive anything is silence wearing a label.

Send a test

POST /v2/notifications/destinations/{id}/test?workspace_id=<uid> — no body.

This is a real send: the same render, the same transport and the same delivery record an alert produces, so a template that works here works during an outage. {{ workspace }} renders the workspace’s display name, exactly as a live alert does.

{ "result": { "message": "Test message delivered", "destination_id": 1,
              "workspace_id": "<uid>" }, "error": null, "meta": null }

A destination that refuses the message answers Destination did not accept the test message (code 2004) with the far end’s own words — the request was fine; the destination was not. The attempt is recorded either way.

Delete a destination

DELETE /v2/notifications/destinations/{id}?workspace_id=<uid>

Deleting the default promotes nobody: a workspace may have none, and handing the role to whichever destination is next would start sending a channel alerts it never asked for. Subject selections naming it go with it; delivery records stay.

Transformation object

{
  "id": 1,
  "name": "PagerDuty compact",
  "template": "{\"event\": \"{{ kind }}\", \"summary\": \"{{ summary }}\"}",
  "content_type": "application/json",
  "workspace_id": "<uid>",
  "created_at": "2026-09-19T08:33:48Z",
  "updated_at": "2026-09-19T08:33:48Z",
  "kind": "notification_transformation"
}

content_type is what a body of this shape is sent as, so a client can see whether its template reads as JSON without guessing.

List transformations

GET /v2/notifications/transformations?workspace_id=<uid> — by name.

Read the samples

GET /v2/notifications/transformations/samples — no workspace, no parameters. The three messages every template is judged against, each with every field it may read:

[{ "alert_kind": "problem", "label": "Problems and reminders",
   "fields": { "kind": "problem", "status": "down", "subject": "Checkout API",
               "url": "https://checkout.example/health", "summary": "…", "error": "…",
               "locations": "de, fr", "lasted": "2m", "link": "https://…",
               "workspace": "Acme ops", "at": "2026-09-16T14:04:00Z" } }]

These are the same fixtures the editor shows and the same ones a save is judged against, so a client can render locally and get the answer this API would give.

Preview a template

POST /v2/notifications/transformations/preview?workspace_id=<uid>

{ "template": "{\"detail\": {{ error }}}" }

Nothing is stored. passed is exactly the condition a create or an update enforces:

{ "result": { "passed": false, "results": [
    { "alert_kind": "problem", "label": "Problems and reminders", "ok": true, "output": "…" },
    { "alert_kind": "recovery", "label": "Recoveries", "ok": false,
      "error": "the rendered body is not valid JSON" } ],
    "kind": "notification_transformation_preview" }, "error": null, "meta": null }

Create a transformation

POST /v2/notifications/transformations?workspace_id=<uid>

{ "name": "PagerDuty compact", "template": "{\"event\": \"{{ kind }}\"}" }

It is stored only if it renders all three samples. There is no force flag; a refusal answers Template does not render every message (code 2001) and names the sample that broke. Ask preview first if you want to know before you write.

Get a transformation

GET /v2/notifications/transformations/{id}?workspace_id=<uid>

Update a transformation

POST /v2/notifications/transformations/{id}?workspace_id=<uid> — same body, same rule. A refused edit leaves the stored template exactly as it was.

Delete a transformation

DELETE /v2/notifications/transformations/{id}?workspace_id=<uid> — destinations using it fall back to Uptimer’s built-in message.

A subject’s alert delivery

Which destinations one subject tells, and about what. Each kind of subject carries the table on the collection that owns it:

SubjectPath
Custom/v2/subjects/{subject}/delivery
Website monitor/v2/monitoring/websites/{id}/delivery

GET returns the whole table, and what an empty one means right now:

{ "result": { "subject_id": "payments-worker",
    "selections": [ { "destination_id": 2, "destination_name": "Pager relay",
                      "alert_kinds": ["problem", "no_data"] } ],
    "default_destination_id": 1, "fallback": "", "workspace_id": "<uid>",
    "kind": "subject_alert_delivery" }, "error": null, "meta": null }

fallback is filled only when selections is empty, and says which of the two things that means: workspace_default or silence. A subject with rows of its own does not also send to the default.

POST replaces the table — the resource is the table, so what you send is what the subject will have. A merge would make a removed row reappear, which is the bug an operator reports as “it keeps notifying the channel I deleted”.

{ "selections": [ { "destination_id": 2, "alert_kinds": ["problem", "no_data", "recovery"] } ] }

alert_kinds is any of problem, no_data, recovery — at least one. A reminder rides with problem, which is why there is no fourth value. An empty selections is a real instruction (“use the workspace default, or send nothing”) and is the same thing as DELETE.

DELETE clears it.

AnswerWhen
Invalid alert kind (code 2001)A word that is not one of the three.
No alert kinds (code 2001)A row with an empty alert_kinds; give it one or leave the row out.
Duplicate destination (code 2001)The same destination listed twice — one row per destination.
Destination not found (code 2002)A destination that is not in this workspace.
Monitor has no subject (code 2002)A Website monitor from before monitoring subjects; its alerts follow the workspace default.
Access denied (code 2005)Writing needs the same permission as editing that subject.

Saving a delivery table changes only where the next alert goes: signals, rules, incidents, acknowledgement and maintenance are untouched, and nothing is sent by saving.

The delivery log

GET /v2/notifications/deliveries?workspace_id=<uid>&destination_id=<id>&undelivered=true

The same rows the Delivery page shows, with the same two filters. Records are kept 30 days.

{ "result": [ { "id": 12, "destination_id": 1, "destination_name": "Acme · #incidents",
    "destination_type": "slack", "alert_kind": "problem", "status": "delivered",
    "payload": "{\"attachments\":[…]}", "at": "2026-09-19T08:33:48Z",
    "kind": "notification_delivery" } ], "error": null, "meta": null }

destination_name and destination_type are the destination as it was at the attempt — the log outlives the destination it describes, so a rename or a delete leaves the record still saying where the message went. error carries the far end’s words on a failure. The webhook URL is never recorded.

v1 methods (frozen)

These routes, fields and kinds are unchanged from earlier releases and stay supported. New integrations should use the v2 methods above.

List workspaces (v1)

GET /v1/workspaces

No parameters, no body. Identical to GET /v2/workspaces — same handler, same payload:

[{ "id": "<uid>", "name": "Default", "role": "owner", "kind": "workspace" }]

List rules

GET /v1/rules?workspace_id=<uid>

ParameterRequiredMeaning
workspace_idyesThe workspace to read. Missing → Missing workspace ID (code 2004).

Returns a list of stored rule objects. A workspace id that does not exist answers Workspace not found (code 2002); one the key’s owner is not a member of answers Access denied (code 2005).

The v2 equivalent is GET /v2/monitoring/websites, which returns the same rules with locations and an agreement.

Create a rule

POST /v1/rules

Body: a rule object without id. workspace_id is required.

regions takes region names, matched as v2 matches location names; an unknown name answers invalid regions / Unknown region: "nope" (code 2001). Omit it and the rule is created with none, which leaves it at “No Data”.

There is no agreement field in v1. A rule created here uses the default agreement, majority, and you change it from the dashboard or through POST /v2/monitoring/websites/{id}.

Returns the stored rule, including its new id.

Get a rule

GET /v1/rules/{uid}

{uid} is the rule’s id. No body. Returns one stored rule object.

A uid that does not exist, or one the key cannot reach, answers Rule not found / Rule does not exist or access denied (code 2002).

Update a rule

POST /v1/rules/{uid} — there is no PUT.

Body: a rule object without id or workspace_id. Like v2, this is a full replacement.

Omitting regions clears them. An update that does not carry the field leaves the rule assigned to no region, and it stops being checked until you assign one again. Send the full list you want to keep.

A uid that does not exist answers Rule not found / Rule does not exist (code 2002). Returns the stored rule.

Delete a rule

DELETE /v1/rules/{uid}

No body. Requires an editor or owner role in the rule’s workspace; a viewer gets Access denied (code 2005).

{ "message": "Rule deleted successfully", "rule_id": "<uid>" }

As with the v2 delete, this removes the rule and the subject, signal and history that belong to it. Deleting a uid that is already gone answers Rule not found (code 2002).

List regions

GET /v1/regions

No parameters, no body. The v1 name for what v2 calls locations — the same instance-wide list, with the same active-worker counts, under the v1 kind:

[{ "id": "<uid>", "name": "eu-west", "active_workers_count": 0, "kind": "region" }]

Errors

{ "result": null,
  "error": { "code": 2001, "error_type": "validation_error", "message": "…", "details": "…" },
  "meta": null }

Branch on code, not on error_type. code is stable and specific:

codeMeaning
10011007Auth: missing, malformed, invalid or expired token.
2001Validation error.
2002Not found.
2003Forbidden.
2004Invalid request — a required query parameter is missing.
2005Access denied — the key’s owner is not a member of that workspace.
2006Malformed JSON.
500Internal error.

error_type is one of access_denied, validation_error, not_found, forbidden, internal_error, but it is coarser than the code: only 2001, 2002, 2003 and the 1xxx auth codes map to their own type — 2004, 2005, 2006 and 500 all report internal_error. That is a v1 quirk, and v2 inherits it deliberately rather than making the two versions disagree about the envelope.

v2 speaks v2’s words in errors too: an unknown location name answers invalid locations / Unknown location: "…", never “region”.

Prefer a typed client? See the Python SDK.