---
title: "Update monitor"
method: PUT
path: "/api/v1/monitors/{monitorID}"
tags: ["Monitors"]
---

# Update monitor

`PUT /api/v1/monitors/{monitorID}`

## Request

- Base URL: `https://api.uptimemonitoring.com`
- URL: `https://api.uptimemonitoring.com/api/v1/monitors/{monitorID}`
- Auth: HTTP bearer

Partially updates a monitor. Only include fields you want to change.
Unchanged fields retain their current values. An explicit
`"type"` other than `http`, `tcp`, or `ping` is rejected with `400`
(issue #209), the same rule `POST /api/v1/monitors` enforces;
omitting `type` leaves the stored value untouched and re-validates
nothing about it.

Per-account scan heuristic (issue #335): a request that changes
`url` or `type` is evaluated by the identical port-fan/host-fan
heuristic `POST /api/v1/monitors` uses — see that endpoint's
description for the exact signatures, the `SCAN_PORTFAN_TCP_THRESHOLD`
TCP-widened ceiling, and the sliding window. A request that leaves
both `url` and `type` unchanged never consults the heuristic at
all. A port-fan trip returns `403` (see "Account suspended" below);
a host-fan trip returns `429` with the same `HostFanError` body and
headers `POST /api/v1/monitors` documents (see "Rate limit
exceeded" below) — this endpoint's 429 never includes the
creation-cap case, since there is no per-route creation limiter on
`PUT`.

## Path parameters

- `monitorID` integer, required

## Request body

- MonitorInput: All string fields must be valid UTF-8 with no null bytes. URL fields must additionally be free of ASCII control characters (U+0000..U+001F and U+007F). Violations return HTTP 400.
  - `name` string
  - `url` string: Target URL. Format depends on `type`: * `http` — full http(s) URL. * `tcp` — `host:port` or `tcp://host:port`, port restricted to a fixed allowlist (issue #331): 21, 22, 25, 80, 110, 143, 443, 465, 587, 993, 995, 2222, 3000, 3306, 5000, 5432, 6379, 8000, 8080, 8443, 8888, or 27017. Every create, and every update whose body includes `url` or `type`, stores the canonical `tcp://host:port` form (lowercase, no path) regardless of which shape was sent — `example.com:443`, `tcp://example.com:443`, and `tcp://EXAMPLE.com:443/` all store as `tcp://example.com:443`. An update that changes neither `url` nor `type` leaves the stored URL as it is. A bracketed IPv6 literal is required (`[2001:db8::1]:443`); an unbracketed one is rejected as ambiguous. * `ping` — a bare hostname or IP literal (e.g. `api.example.com`, `1.1.1.1`) — no scheme, path, query, userinfo, or port. * `dns` / `ssl` — reserved formats (a bare hostname for `dns`) for a `type` that is defined but not yet serviced by the check engine; see the `type` field.
  - `type` 'http' | 'tcp' | 'ping': `http`, `tcp`, and `ping` are accepted on create or update. `dns` and `ssl` are defined in the wire format but rejected with `400` (issue #209): `type must be one of: http, tcp, ping, dns, ssl (currently available: http, ping, tcp)` for an unrecognized value, or `type '<type>' is not available yet; the check engine currently supports: http, ping, tcp` for one of those two.
  - `interval_sec` integer: Check interval in seconds. Default 60.
  - `expected_status` string: HTTP status range. Default: 200-399. Accepted but silently ignored for `type: tcp` or `type: ping` (neither has an HTTP response to evaluate it against).
  - `body_match` string: Optional string to match in the response body. HTTP only — rejected with `400` (`body_match is not applicable to tcp monitors`) when `type` is `tcp`. Accepted but silently ignored (a harmless no-op; wopher's ping check never reads it) when `type` is `ping`.
  - `enabled` boolean: Default true.
  - `maintenance` MaintenanceWindow: A monitor's single alert-silencing window (issue #361). Always present on a `Monitor`; `{"type":"none"}` (the default) silences nothing. Accepted on `MonitorInput`/`MonitorCreateInput` as one of three discriminated shapes — each accepts ONLY the fields listed for it, plus `type`; any other field (including one that belongs to a different shape) is `400` naming it, e.g. `maintenance.timezone: not allowed for type "once"`. * `none` — no other fields. * `daily` — `start_time`, `end_time`, `timezone` all required. Repeats every day in that IANA zone; may span midnight (`start_time > end_time`). `start_time` must differ from `end_time`. The zone must be a real IANA name — empty, `Local`, and anything `LoadLocation` can't resolve are all rejected. * `once` — `starts_at`, `ends_at` both required, `ends_at` after `starts_at`. A single absolute span; no zone, since both instants are already pinned by their offsets. Both timestamps are normalized to UTC at microsecond precision (PostgreSQL's `timestamptz` precision) before being validated or stored, so two timestamps that differ only below a microsecond collapse to equal and are rejected as `ends_at` not being after `starts_at`. On `PUT`, omitting `maintenance` entirely leaves the stored window unchanged; an explicit JSON `null` is `400` (`maintenance must not be null`) — the same rule `recipients` follows elsewhere. `POST` omitting it defaults to `{"type":"none"}`.
    - `type` 'none' | 'daily' | 'once'
    - `start_time` string: "HH:MM", daily only. Canonical: zero seconds, below 24:00.
    - `end_time` string: "HH:MM", daily only.
    - `timezone` string: IANA zone name, daily only.
    - `starts_at` string, date-time: RFC3339, once only. Echoed back UTC-normalized at microsecond precision.
    - `ends_at` string, date-time: RFC3339, once only. Echoed back UTC-normalized at microsecond precision.
  - `method` 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS': HTTP request method (issue #364). Default `GET`. HTTP only — sending this field at all (including its default value) on a `tcp` or `ping` create/update is `400` (`method is only applicable to http monitors`). On update, omitting it leaves the stored method unchanged; an explicit `null` is `400` (`method must not be null`) — but only when `null` is the value the request body actually decodes to: exact-duplicate JSON keys (same spelling) resolve via `encoding/json`'s last-value-wins before this check ever runs, so `{"method":null,"method":"POST"}` is accepted as `POST`; only a differently-cased duplicate (e.g. `Method` alongside `method`) is itself rejected, as more than one key for the same field. The same caveat applies to `body`/`timeout_sec` below, which share this rejection rule.
  - `body` string: Request body (issue #364). Default `""` — a bodiless POST is allowed. Must be valid UTF-8 with no null bytes. HTTP only, same rejection rule as `method`. Cleared by sending `""` on update.
  - `headers` object[]: Custom request headers (issue #364), at most 20 entries and 8192 bytes total (`len(name)+2+len(value)` summed). HTTP only, same rejection rule as `method`. Values must be valid UTF-8 with no CR, LF, NUL, or other control character except horizontal tab. On update, sending `url` with a different scheme, lowercased host, or effective port than the stored URL requires every resulting header to carry a `value` in THIS request — an omitted `headers` field (when headers are stored) or any kept-by-name entry is `400` (`headers must be resent with their values when the URL's scheme, host, or port changes`), closing the path where one account member could repoint another member's monitor (with its still-live secret header) at a host they control. An empty array (`[]`) clears every header.
    - `name` string, required: An RFC 7230 token. `Host`, `Content-Length`, `Transfer-Encoding`, and `Connection` are rejected case-insensitively, as are two entries with the same name (case-insensitive). This is a check across the headers array's entries, not of this field's own JSON key spelling within one entry — see this schema's own description above for that.
    - `value` string: On `PUT`, an entry with `value` omitted keeps the currently stored value for that name (case-insensitive match) — the array as a whole still REPLACES the full stored list, so omitting `headers` entirely (not sending an entry at all) is what leaves every header untouched. A name with no stored value and no `value` sent is `400`. An explicit `null` is `400` (`header "<name>" value must not be null`) when `null` is the value `encoding/json` resolves this field to — see this schema's own description above for how a same- or differently-cased duplicate `value`/`Value` key within one header object resolves instead (last occurrence wins, never rejected). On `POST`, every entry must carry a `value` — there is nothing stored yet to keep.
  - `timeout_sec` integer: Request timeout in seconds (issue #364). Default 30. HTTP only, same rejection rule as `method`.

## Response `200`

Monitor updated.

- MonitorDetail
  - `monitor` Monitor
    - `id` integer
    - `account_id` integer
    - `name` string
    - `url` string: Target URL. Format depends on `type`: * `http` — full http(s) URL, e.g. `https://api.example.com/health`. * `tcp` — always returned/stored as `tcp://host:port`, port restricted to a whitelist (see `MonitorInput.url` for the accepted input shapes). * `ping` — a bare hostname or IP literal (e.g. `api.example.com`, `1.1.1.1`) — no scheme, path, query, userinfo, or port. * `dns` / `ssl` — reserved formats (a bare hostname for `dns`) for a `type` that is defined but not yet serviced by the check engine; see the `type` field.
    - `type` 'http' | 'tcp' | 'ping': `http`, `tcp`, and `ping` can appear here. `dns` and `ssl` are reserved for a check engine that does not exist yet (issue #209); no monitor of one of those two types can be created, updated, or persisted.
    - `interval_sec` integer
    - `expected_status` string: Single HTTP status code, expressed as a string, 100–599. Defaults to "200".
    - `body_match` string, nullable
    - `enabled` boolean
    - `response_time_threshold_ms` integer, nullable: Alert if response time exceeds this (ms). Null = disabled.
    - `maintenance` MaintenanceWindow: A monitor's single alert-silencing window (issue #361). Always present on a `Monitor`; `{"type":"none"}` (the default) silences nothing. Accepted on `MonitorInput`/`MonitorCreateInput` as one of three discriminated shapes — each accepts ONLY the fields listed for it, plus `type`; any other field (including one that belongs to a different shape) is `400` naming it, e.g. `maintenance.timezone: not allowed for type "once"`. * `none` — no other fields. * `daily` — `start_time`, `end_time`, `timezone` all required. Repeats every day in that IANA zone; may span midnight (`start_time > end_time`). `start_time` must differ from `end_time`. The zone must be a real IANA name — empty, `Local`, and anything `LoadLocation` can't resolve are all rejected. * `once` — `starts_at`, `ends_at` both required, `ends_at` after `starts_at`. A single absolute span; no zone, since both instants are already pinned by their offsets. Both timestamps are normalized to UTC at microsecond precision (PostgreSQL's `timestamptz` precision) before being validated or stored, so two timestamps that differ only below a microsecond collapse to equal and are rejected as `ends_at` not being after `starts_at`. On `PUT`, omitting `maintenance` entirely leaves the stored window unchanged; an explicit JSON `null` is `400` (`maintenance must not be null`) — the same rule `recipients` follows elsewhere. `POST` omitting it defaults to `{"type":"none"}`.
      - `type` 'none' | 'daily' | 'once'
      - `start_time` string: "HH:MM", daily only. Canonical: zero seconds, below 24:00.
      - `end_time` string: "HH:MM", daily only.
      - `timezone` string: IANA zone name, daily only.
      - `starts_at` string, date-time: RFC3339, once only. Echoed back UTC-normalized at microsecond precision.
      - `ends_at` string, date-time: RFC3339, once only. Echoed back UTC-normalized at microsecond precision.
    - `method` 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS': HTTP request method (issue #364). Always `GET` for `tcp`/`ping`/ `dns`/`ssl` monitors, regardless of what might once have been stored — a non-http monitor's custom-request fields are always reset to their defaults on write.
    - `body` string: Request body, verbatim (issue #364). Always `""` for a non-http monitor. Unlike `headers`, the body is NOT write-only — it is returned verbatim on every read to anyone who can read the monitor. It must not contain credentials or other sensitive data unless that exposure is acceptable; `headers` is the write-only place for secrets.
    - `headers` RequestHeader[]: Custom request headers (issue #364), write-only: each entry reports only `name` and `value_set`, never the value itself. Always `[]` for a non-http monitor.
      - `name` string, required
      - `value_set` boolean, required: Whether this header carries a non-empty value.
    - `timeout_sec` integer: Request timeout in seconds (issue
    - `created_at` string, date-time
    - `updated_at` string, date-time
  - `state` MonitorState
    - `monitor_id` integer
    - `status` 'up' | 'down' | 'unknown' | 'flapping'
    - `last_check_at` string, date-time, nullable
    - `last_transition_at` string, date-time, nullable
    - `suspect_since` string, date-time, nullable
    - `flap_count_1h` integer
    - `flapping` boolean
    - `evidence_buffer` CheckResult[]
      - `timestamp` string, date-time
      - `region` string
      - `status_code` integer
      - `error_class` string
      - `dns_ms` number
      - `tls_ms` number
      - `connect_ms` number
      - `ttfb_ms` number
      - `download_ms` number
      - `content_length` integer
      - `response_hash` string
      - `packet_loss_pct` number: Ping-only packet loss percentage (0-100). Omitted (not present in the object) when not measured — every non-ping check result, and a ping probe-side failure that never reached the target. A measured 0% loss is present with value 0, distinct from omitted.
    - `primary_region` 'EU' | 'US-E' | 'US-W' | 'ASIA' | 'OCE' | 'SA' | 'null', nullable: The region assigned as primary for this monitor's checks. Null until the first check is dispatched.
  - `notice` SoftCapNotice: Advisory attached to `POST /api/v1/monitors` when the post-insert per-account monitor count falls in the soft-cap window (51..100). Clients should branch on `code` rather than parsing `message`.
    - `code` 'soft_cap_reached', required
    - `message` string, required: Human-readable advisory text. Wording is set by SPEC and may evolve.
    - `monitors_current` integer, required: Per-account monitor count after the create.
    - `soft_cap` integer, required: Soft cap threshold; crossing it triggers the notice.
    - `hard_cap` integer, required: Hard cap; reached → 409 on subsequent creates.
  - `last_check_skipped` string: Populated only on `POST /monitors` when the create-time initial check did not produce a usable result — either short-circuited before any probe ran, or the probe started but did not finish within the create-time budget, leaving the first check pending. The state row's `last_check_at` remains `null` in either case; clients distinguish "no data yet" from "skipped on purpose" via this field. Vocabulary: * `domain_rate_limited` — the per-domain dispatch cap (`DomainLimiter`) consumed the slot; the next scheduled tick will retry once the bucket refills. * `initial_check_timeout` — the create-time probe did not return within the server's bounded wait; the monitor is registered normally and the next scheduled tick resolves it.
  - `recipient_ids` integer[]: Attached recipient ids (issue #212). Always present, `[]` for none. On `POST /monitors`, reflects any account default recipients attached inside the create transaction — though a monitor born down does not notify them at creation; see `Recipient.default`.

## Other responses

- `400`: Invalid request — including an unsupported `type` (issue #209) and the same field-level and URL rules `POST /api/v1/monitors` enforces.
- `401`: Missing or invalid credentials.
- `403`: Account is currently suspended. On `POST /api/v1/monitors`, and on a `PUT /api/v1/monitors/{monitorID}` that changes `url` or `type` (issue #335), this can also be returned by the per-account port-scan heuristic when the triggering request crossed the port-fan threshold inside the sliding window (10 minutes by default; `SCAN_WINDOW`). A host-fan trip does NOT suspend the account — see the 429 response instead.
- `404`: Monitor not found.
- `409`: Conflict. Either the new URL duplicates another monitor on this account, or the monitor was modified concurrently by another request and retries were exhausted — retry the request with a fresh read in the latter case.
- `413`: Request body exceeds the 64 KB hard limit. Per docs/SPEC.md §Hard Limits the dispatcher rejects any payload larger than 65,536 bytes before it is decoded.
- `415`: The request `Content-Type` is not `application/json`. Write operations (POST, PUT, PATCH) require that header; a missing or non-JSON media type is rejected before the body is read. The `charset` parameter is accepted and ignored, so `application/json; charset=utf-8` is valid.
- `429`: Per-account dispatcher rate limit exceeded on `PUT /api/v1/monitors/{monitorID}` (issue #335). Two possible causes, distinguishable by `X-RateLimit-Reason` header (absent on the global-cap rejection) — no creation-cap case, since `PUT` has no equivalent to `POST`'s 20/min creation limiter: 1. **Global cap** (200/min per account): `error` contains `"account rate limit exceeded; retry in <N>s"`. `X-RateLimit-Limit: 200`. 2. **Host-fan** (≥8, by default — `SCAN_HOSTFAN_THRESHOLD` — distinct registrable domains in 10 min, by default — `SCAN_WINDOW`), tripped by a request that changes `url` or `type`: body is the `HostFanError` schema with `retry_after_seconds` and `limit`. `X-RateLimit-Reason: host_fan`. Account is NOT suspended. `X-RateLimit-Limit`/ `X-RateLimit-Window` and the body's `limit`/`window_seconds` always reflect the actual configured values. The `Retry-After` header is present in both cases (integer >= 1).

---

[API](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api.md) · [All operations](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api/llms.txt) · [OpenAPI document](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api/revisions/ddcb2a8bc70f?raw)
