---
title: "Update a check"
method: PATCH
path: "/api/v1/checks/{id}"
tags: ["Checks"]
---

# Update a check

`PATCH /api/v1/checks/{id}`

## Request

- Base URL: `https://app.lastping.dev`
- URL: `https://app.lastping.dev/api/v1/checks/{id}`
- Auth: API key in header `X-Api-Key`

Partially updates a monitor using JSON Merge Patch semantics (RFC 7396):

- **Omitted field** — the stored value is left unchanged.
- **Explicit `null`** — clears the value. Supported on `runaway_ceiling`,
  `max_runtime_s`, `step_timeout_s`, `blocked_timeout_s`,
  `expect_every_s`, `notify_min_run_s`,
  `monitor_from`, `ci_workflow`, `ci_branch`, `tags`, `agent_id`,
  `source_kind` and `source_ref` only — the same thirteen properties
  typed to allow `null` in the `CheckPatch` schema; for `tags`, explicit
  `null` is equivalent to sending `[]`; for `blocked_timeout_s`,
  explicit `null` does not mean "wait forever" — it restores the 24h
  default, same as omitting the field;
  for `agent_id`, explicit `null`
  detaches the monitor from its agent without deleting either of them;
  for the source pair, `null` must be sent on **both** keys, since a
  half-cleared discovery identity reconciles against nothing and is
  rejected with 400 (`SOURCE_INCOMPLETE`).
  `failure_threshold` is NOT clearable (it always has a value,
  default 1); a `null` there is treated as omission.
- **Any other value** — replaces the stored value.

Changing `failure_threshold` does **not** reset `consecutive_failures`:
lowering it means "that many failures is now enough", so a monitor
already at or above the new threshold alerts on its next failure.

`tags` is replaced wholesale, not merged; send `[]` or `null` to remove
all tags. `slug`, `monitor_type` and `ci_secret` are immutable and are
ignored if present.  `ci_provider` is immutable too, but on a monitor
with no CI binding it is now rejected with 400 (`FIELD_NOT_IN_SHAPE`)
rather than accepted and ignored — see the `CheckPatch` schema.

`ci_workflow`, `ci_branch`, `source_kind` and `source_ref` deliberately
deviate from strict RFC 7396: an explicit empty string `""` preserves
the stored value, the same as omitting the field. This protects
full-body clients (e.g. Terraform) that always send `""` for an unset
attribute from silently unbinding a live CI filter — or, for the source
pair, from destroying a monitor's discovery identity, after which the
next scan no longer recognises the source and creates a **second**
monitor for it. Explicit `null` is the documented way to clear any of
these fields.

Sending a complete body remains valid and behaves as a full replace.

## Request body

- CheckPatch: Body for updating a check (PATCH /api/v1/checks/{id}), using JSON Merge Patch semantics (RFC 7396). This is deliberately NOT `CheckCreate`: on create an omitted field means "use the default", so `name` is required and nothing is nullable; on update an omitted field means "leave the stored value unchanged", and an explicit `null` is the only way to clear a value. - **No field is required** — a body may contain a single key. - **Omitted field** — the stored value is left unchanged. - **Explicit `null`** — clears the value. Accepted only by `runaway_ceiling`, `max_runtime_s`, `step_timeout_s`, `blocked_timeout_s`, `expect_every_s`, `notify_min_run_s`, `monitor_from`, `ci_workflow`, `ci_branch`, `tags`, `agent_id`, `source_kind` and `source_ref`; those are the thirteen properties typed to allow `null` below. `ci_workflow`, `ci_branch`, `source_kind` and `source_ref` treat an explicit empty string `""` as **omission**, not as a clear — see the operation description for `PATCH /api/v1/checks/{id}`. For `agent_id`, explicit `null` detaches the monitor from its agent — it does not delete the agent or the monitor. For `blocked_timeout_s`, explicit `null` does NOT mean "wait forever" — it restores the fallback to the default (24h), same as omitting the field entirely. `source_kind` and `source_ref` are cleared as a PAIR — nulling one alone is a 400 (`SOURCE_INCOMPLETE`), because half a discovery identity reconciles against nothing. - **Any other value** — replaces the stored value. `slug`, `monitor_type` and `ci_secret` are immutable on update and are ignored if present. `ci_provider` is immutable too, but it is **not** ignored: it is not a member of this schema, so shape validation sees it. On a monitor that already has a CI binding it is part of the shape and accepted as a no-op; on a monitor with **no** binding the shape is `heartbeat`, which does not declare it, so `{"ci_provider":"github"}` is rejected with 400 (`FIELD_NOT_IN_SHAPE`). Rebinding a monitor to a CI system means deleting and recreating it; PATCH was never able to do it, and now says so instead of returning 200 as though it had. **Every field you send is validated against the monitor's SHAPE**, the same way it is on create: a field the stored `monitor_type` and the resulting `schedule_kind` have no place for is rejected with 400 (`FIELD_NOT_IN_SHAPE`) rather than merged and discarded. So `schedule_kind`/`period_s`/`cron_expr`/`tz` are refused on an `http` monitor (its schedule is re-derived from `probe_interval_s` on every PATCH) and `ci_workflow`/`ci_branch` are refused on a monitor with no `ci_provider` bound. This is keyed off **what you send**, never off the stored row: `null`, `""`, `0`, `false` and empty collections carry no intent and are ignored, so a client that PATCHes a full document — an explicit `null` for every clearable field, `""` for every unset string — keeps applying cleanly, and no stored value can strand a monitor on a later rename. **A value equal to the one `GET` already reports is also ignored.** A monitor whose `probe_timeout_s` is 10 accepts `{"probe_timeout_s": 10}` whatever its type: the request asks for no change, so nothing can be accepted and discarded. This matters because every monitor carries probe defaults (`probe_method` `"GET"`, `probe_expected_status` 200, `probe_timeout_s` 10, `probe_follow_redirects` `true`) that a `GET` returns for **every** monitor type, and a client that round-trips a `GET` into a `PATCH` — Terraform does exactly this, carrying prior state into the plan for every attribute the configuration leaves unset — would otherwise be refused over keys it never chose. A value that **differs** from the stored one is still rejected, and `POST /api/v1/checks` has no such tolerance: on create there is nothing to echo. The schedule kind is the **merged** one, so a PATCH that switches a monitor from `simple` to `cron` is validated against `cron` and may carry `cron_expr`.
  - `name` string: Human-readable name for the check. A new name must be at most 200 characters (not bytes) and free of control and invisible characters (400 `INVALID_NAME`); sending the stored name unchanged is always accepted. Bytes that are not valid UTF-8 are replaced with U+FFFD when the request is decoded, and accepted.
  - `schedule_kind` 'simple' | 'cron' | 'on_demand': `simple` uses a fixed period; `cron` uses a cron expression; `on_demand` means the check has no cadence at all — it is driven entirely by run events, never arms an absence deadline, and silence between runs never alerts. Run-scoped detection (`max_runtime_s`, `step_timeout_s`) still applies once a run starts, and `expect_every_s` — the silence floor — is the only way to detect absence between runs. Ignored when the monitor's type is `http` (schedule is driven by `probe_interval_s`).
  - `period_s` integer: Expected ping interval in seconds. Applies when schedule_kind is `simple`. **Must be absent when the resolved schedule_kind is `on_demand`** — sending it in the same request that sets (or leaves) the check at `on_demand` is rejected with 400 (`ON_DEMAND_SCHEDULE_CONFLICT`).
  - `cron_expr` string: Cron expression (5-field). Applies when schedule_kind is `cron`. **Must be absent when the resolved schedule_kind is `on_demand`** — sending it in the same request that sets (or leaves) the check at `on_demand` is rejected with 400 (`ON_DEMAND_SCHEDULE_CONFLICT`).
  - `tz` string: IANA timezone for cron evaluation.
  - `grace_s` integer: Grace period in seconds after a ping is due before the check is considered late. For an `http` monitor the effective grace is derived as 2 × `probe_interval_s`; a larger explicit `grace_s` sent in the same request is honoured instead. `grace_s` is the silence tolerance and the first-run deadline; the overrun deadline uses `max_runtime_s` when that is set.
  - `max_runtime_s` integer, nullable: Run-duration budget in seconds for the **overrun deadline only**; the silence rule and the first-run seed keep using `grace_s`. Time a run spends `blocked` does not count toward it. Explicit `null` clears it, restoring the fallback to `grace_s`; omitting the field leaves it as stored. **Rejected with 400 (`MAX_RUNTIME_NOT_SUPPORTED`) on an `http` monitor** when sent with a non-null value — an HTTP probe has no start/success pair, so the overrun rule can never fire. An explicit `null` on an `http` monitor is accepted as a no-op.
  - `step_timeout_s` integer, nullable: Progress budget in seconds for the **stall rule only**. Explicit `null` disables stall detection; omitting the field leaves it as stored. Stall detection needs your job to call /step: the Claude Code hook and the reporting prompt do not send steps, so leave step_timeout_s unset for them. The merged value must stay strictly below the merged effective run budget `COALESCE(max_runtime_s, grace_s)`, and this is checked from BOTH directions: a PATCH that only raises `grace_s`, or only clears `max_runtime_s`, is rejected with 400 (`STEP_TIMEOUT_EXCEEDS_BUDGET`) when it would push the stored `step_timeout_s` out of the window. **Rejected with 400 (`STEP_TIMEOUT_NOT_SUPPORTED`) on an `http` monitor** when sent with a non-null value. An explicit `null` on an `http` monitor is accepted as a no-op.
  - `expect_every_s` integer, nullable: **Silence floor** in seconds: alert if no ping of any kind arrives within this window, regardless of the schedule. Explicit `null` turns the floor off; omitting the field leaves it as stored. On an `on_demand` monitor, clearing it returns the monitor to arming **no absence deadlines at all** between runs. Accepted on every `monitor_type`, including `http`.
  - `notify_min_run_s` integer, nullable: **Notification duration floor**: a run shorter than this many seconds does not produce an INFO-CLASS notification (`success`, `started`, `every-run`, `note`). Never suppresses `down`, `fail`, `recovery` or `blocked`, and never suppresses `started` or an event whose duration is unknown. Explicit `null` turns the floor off; omitting the field leaves it as stored. **Rejected with 400 (`NOTIFY_MIN_RUN_NOT_SUPPORTED`) on an `http` monitor** when sent with a non-null value — an HTTP probe has no start/success pair, so its run duration is never measured. An explicit `null` on an `http` monitor is accepted as a no-op.
  - `blocked_timeout_s` integer, nullable: How long (in seconds) a check may sit `blocked` before a `blocked` incident opens. **Explicit `null` does NOT mean "wait forever"** — it clears the stored value and falls back to the default (24 hours), same as omitting the field on a check that never set it. There is no way to disable the blocked timeout entirely. Accepted on every `monitor_type`, including `http`.
  - `failure_threshold` integer: Consecutive explicit failures required before an incident opens. Applies to the `fail` cause ONLY — `silence`, `overrun`, `never_started`, `runaway`, `stalled` and `blocked` are time- or rate-based and are never gated by it. A run that ends on the model provider's API error (`upstream`) neither counts toward it nor resets it: those page on their own count of runs in a row, which is 3 or this threshold, whichever is higher. Not nullable: it always has a value (default 1), so there is no "unset" state to return to. An omitted field (or an explicit `null`, treated the same as omission) leaves the stored value unchanged. **Changing it does NOT reset `consecutive_failures`.** With the counter at 3 and the threshold lowered from 5 to 2, the very next failure opens an incident. This is deliberate: the counter is a factual record of failures that really happened, and lowering the threshold means "that many is now enough" — resetting would discard true information. Raising it likewise keeps the counter, so a monitor already past the new threshold alerts on its next failure.
  - `monitor_from` string, date-time, nullable: RFC 3339 timestamp. Deadlines are not computed before this time. Explicit `null` clears it.
  - `probe_url` string, uri: Absolute `http` or `https` URL for LastPing to probe (`http` monitors).
  - `probe_method` 'GET' | 'HEAD' | 'POST': HTTP method for the probe request.
  - `probe_expected_body` string: Substring that must appear in the response body for the probe to be considered successful. Send `""` to skip body matching.
  - `probe_interval_s` integer: How often (in seconds) LastPing should probe the URL. Must be between 30 and 86400.
  - `probe_expected_status` integer: HTTP status code the probe must receive to count as success. `0` means "any 2xx response counts as success".
  - `probe_timeout_s` integer: Probe HTTP request timeout in seconds. Must be between 1 and 30.
  - `probe_follow_redirects` boolean: Whether the probe should follow HTTP redirects.
  - `ci_workflow` string, nullable: CI workflow name to filter inbound webhooks by (provider-specific). Deliberately deviates from strict RFC 7396: an explicit empty string `""` preserves the stored value, the same as omitting the field, so that full-body clients (e.g. Terraform) which always send `""` for an unset filter cannot silently unbind a live filter. Explicit `null` clears it.
  - `ci_branch` string, nullable: Branch name to filter inbound webhooks by. Same empty-string preservation rule as `ci_workflow`; explicit `null` clears it.
  - `runaway_ceiling` integer, nullable: Maximum pings allowed in a rolling 1-hour window. Exceeding it opens a `runaway`-cause incident, which detects a looping agent or job pinging abnormally fast. Explicit `null` disables the check; omitting the field leaves it as stored.
  - `tags` string[], nullable: Labels for namespace scoping and auditing. Replaced wholesale, not merged. Max 20 tags; each tag max 50 characters. Duplicate tags are silently deduplicated. Send `[]` or `null` to remove all tags; omitting the field leaves the stored tags alone.
  - `agent_id` string, nullable: The agent that owns this monitor, by `id` or `slug`. Omitting the field preserves the current attachment; an explicit `null` detaches the monitor (sets its `agent_id` to null) without deleting anything; any other non-empty value re-attaches it, resolved fresh on every request. An explicit empty string `""` is rejected with 400 — unlike `ci_workflow`/`ci_branch`, it is NOT treated as "leave unchanged"; omit the field or send `null` instead. **An unknown or foreign agent reference is rejected with 400 (`UNKNOWN_AGENT`)** and never implicitly creates the agent.
  - `source_kind` string, nullable: The scanner that discovered this monitor. Omitting the field — **or sending an explicit empty string `""`** — leaves the stored value alone; only an explicit `null` clears it, and only together with `source_ref`. The `""`-preserves rule matches `ci_workflow`/`ci_branch` and exists for the same full-body clients, with a sharper consequence: every other clearable field here is a *setting*, but this pair is an *identity*. A source cleared by accident is a monitor the next discovery scan no longer recognises, so the scan reports the source as unmonitored and creates a **second** monitor for it — the exact duplicate the reconcile key exists to prevent. **PATCH is the only surface that can change a monitor's source after create.** That is deliberate: a slug upsert body cannot tell "omitted" from "clear", so allowing it to write here would let a slug collision silently adopt a hand-made monitor into discovery. A PATCH names a specific monitor and a specific field, which makes adoption an explicit act — and is exactly what Terraform's import-then-apply flow performs.
  - `source_ref` string, nullable: The stable path within `source_kind`. Same merge-patch rules as `source_kind` above. **The two are resolved as a pair.** Whatever the request and the stored row combine to, the result must be either both set or both empty — `{"source_ref": null}` alone is rejected with 400 (`SOURCE_INCOMPLETE`), not applied as a half-cleared identity. **Clearing therefore takes an explicit `null` on both keys.** A pair another monitor in this project already claims is rejected with 409 (`SOURCE_ALREADY_MONITORED`): two monitors reconciling to one source would make every later discovery run ambiguous. A PATCH that mentions neither key issues no source write at all, so a discovered monitor cannot lose its identity to an unrelated edit such as a rename.
  - `trace_content` 'redacted' | 'dropped': What this monitor's traces keep of prompt, command and tool content. `dropped` (the default) removes the content attributes (prompts, completions, commands, tool arguments, results and definitions, retrieved documents, working directories) of every recognised vocabulary entirely; lengths such as `user_prompt_length`, token counts and cost are kept. Error text (`error`, `error.message`, `exception.message`, `exception.stacktrace` and the span's status message) is kept at `dropped`, redacted and then cut to 512 characters, and a cut marks the span or log event truncated. `redacted` opts in to storing that content, with every secret-shaped value replaced by `[REDACTED:<class>]` at ingest, before anything is queued or stored: API keys (LastPing, Anthropic, OpenAI, GitHub, GitLab, npm, AWS access key ids, Slack, Stripe, Google, Hugging Face, Perplexity, Fireworks, Supabase, New Relic, Airtable, Terraform Cloud, Firebase), HashiCorp Vault tokens, age secret keys, Discord bot tokens, Slack, Discord and Teams webhook URLs, `Authorization` header values, JWTs, private key blocks, `password=`/`token=`/`api_key=` style assignments (subscripted ones such as `os.environ["API_KEY"] = ...` and Dockerfile `ENV`/`ARG` lines included), SQL `PASSWORD '...'` and `IDENTIFIED BY` clauses, the Redis `AUTH` command, `.netrc` passwords, command-line password flags and the argument after them (any secret-named `--flag`, `-p` after a login subcommand, `redis-cli -a`), cookies, credentials in URLs, the value of any attribute or JSON field whose name is secret-named (password, token, api_key, secret, the same word in several other languages, an `Authorization` or `X-Api-Key` header and the like; `Idempotency-Key` and request, correlation and trace ids are not secrets), including JSON carried inside another JSON string such as tool-call arguments, and long high-entropy tokens. A bytes attribute that is valid UTF-8 is scanned and stored as its text. A span that had a value redacted carries `lastping.redacted_values`. A request whose redaction work exceeds a fixed budget stores `[REDACTED:over_budget]` for the values it could not scan, never the values themselves. In both modes every remaining value is redacted the same way. There is no setting that stores content unredacted. A change reaches ingest within 60 seconds: ingest caches each monitor's setting for up to a minute, so an export sent just after the change may still be handled the old way. Absent leaves the stored value alone. `null` and any other value are rejected with 400 (`INVALID_TRACE_CONTENT`): the setting has no unset state. A change reaches the ingest host within a minute.

## Response `200`

Updated check.

- Check: A check resource returned by the API.
  - `id` string, uuid, required: Unique check identifier.
  - `name` string, required: Human-readable name.
  - `slug` string, required: Stable project-scoped identifier (may be empty string if not set).
  - `schedule_kind` 'simple' | 'cron' | 'on_demand', required: `on_demand` means the check has no cadence: it never arms an absence deadline, so silence between runs never alerts. Run-scoped detection (`max_runtime_s`, `step_timeout_s`) still applies once a run starts, and `expect_every_s` — the silence floor — is the only way to detect absence between runs.
  - `period_s` integer, required: Period in seconds (only meaningful for schedule_kind=simple).
  - `cron_expr` string: Cron expression (only present when schedule_kind=cron).
  - `tz` string, required: IANA timezone.
  - `grace_s` integer, required: Grace period in seconds — the silence tolerance and the first-run deadline, and the overrun budget when `max_runtime_s` is unset.
  - `max_runtime_s` integer, nullable: Run-duration budget in seconds for the overrun deadline only. **Omitted from the response when unset**, in which case the overrun rule falls back to `grace_s`. Never set on `http` monitors.
  - `step_timeout_s` integer, nullable: Progress budget in seconds for the stall rule. **Omitted from the response when unset**, in which case stall detection is disabled. Never set on `http` monitors.
  - `expect_every_s` integer, nullable: Silence floor in seconds. **Omitted from the response when unset**, in which case absence is detected only by the schedule — and, for an `on_demand` monitor, not at all between runs.
  - `notify_min_run_s` integer, nullable: Notification duration floor in seconds. **Omitted from the response when unset**, in which case every info-class notification (`success`, `started`, `every-run`, `note`) fires regardless of how short the run was. Never set on `http` monitors. Never suppresses `down`, `fail`, `recovery` or `blocked`.
  - `blocked_timeout_s` integer, nullable: How long (in seconds) the check may sit `blocked` before a `blocked` incident opens. **Omitted from the response when unset** — that does NOT mean the check waits forever, it means the 24-hour default applies.
  - `failure_threshold` integer, required: Consecutive explicit failures required before an incident opens (`fail` cause only). Always present; `1` means "open on the first failure", the default.
  - `monitor_from` string, date-time, nullable: RFC 3339 timestamp from which deadlines are computed.
  - `status` 'new' | 'up' | 'late' | 'down', required: Current check status derived from ping history.
  - `last_ping_at` string, date-time, nullable: RFC 3339 timestamp of the most recent ping received.
  - `due_at` string, date-time, nullable: RFC 3339 timestamp when the next ping is expected.
  - `alert_after` string, date-time, nullable: RFC 3339 timestamp after which an alert is sent if no ping arrives.
  - `paused` boolean, required: When true the check does not alert regardless of ping status.
  - `maintenance_until` string, date-time, nullable: RFC 3339 timestamp until which the check is snoozed. Alerts are suppressed while this is in the future; null or absent when the check is not snoozed.
  - `created_at` string, date-time, required: RFC 3339 UTC timestamp when the check was created.
  - `ping_url` string, uri, required: The URL your service should HTTP-GET to record a successful ping.
  - `monitor_type` 'heartbeat' | 'ci' | 'http': Monitor mode. Omitted on responses when the value is the default `heartbeat`.
  - `probe_url` string, uri: URL being probed (HTTP monitors only; omitted otherwise).
  - `probe_method` 'GET' | 'HEAD' | 'POST': HTTP method used for the probe (HTTP monitors only; omitted otherwise).
  - `probe_expected_body` string: Expected response body substring (HTTP monitors only; omitted otherwise).
  - `probe_interval_s` integer: Probe interval in seconds (HTTP monitors only; omitted otherwise).
  - `probe_expected_status` integer: Expected HTTP status code (HTTP monitors only; omitted otherwise).
  - `probe_timeout_s` integer: Probe timeout in seconds (HTTP monitors only; omitted otherwise).
  - `probe_follow_redirects` boolean: Whether the probe follows redirects (HTTP monitors only; omitted otherwise).
  - `next_probe_at` string, date-time, nullable: RFC 3339 UTC timestamp when the next probe is scheduled (HTTP monitors only; omitted otherwise).
  - `ci_provider` 'github' | 'gitlab' | 'jenkins': CI provider bound to this check (omitted when no CI binding is set).
  - `ci_configured` boolean: True when a CI binding is set on this check (omitted when false).
  - `ci_webhook_url` string, uri: Webhook URL for this CI check (omitted when no CI binding is set). POST this URL from your CI provider with the `ci_secret` as the HMAC secret.
  - `ci_secret` string: **WRITE-ONCE** — returned ONLY in the `201` create response and the `POST /api/v1/checks/{id}/ci/regenerate` response. NEVER returned on GET or list. Copy it immediately; it cannot be retrieved again (use regenerate to rotate).
  - `ci_workflow` string: Optional workflow/job name filter narrowing which CI runs this check watches (omitted when unset, which watches every workflow).
  - `ci_branch` string: Optional branch filter narrowing which CI runs this check watches (omitted when unset, which watches every branch).
  - `ci_ignored` object: Finished CI runs (succeeded or failed) that `ci_workflow` or `ci_branch` filtered out since the last finished run that matched them, or since the filters last changed. Omitted when none were. A count that keeps growing with no recent match usually means a filter no longer matches anything, for example after a workflow rename or a default-branch change. A matching finished run and a change to either filter both reset it. A repository webhook sends the runs of every workflow, so with `ci_workflow` set, `count` includes other workflows' runs, which the filter is meant to drop; `same_workflow_count` counts only the runs of `ci_workflow` (or of any workflow when it is not set) that `ci_branch` dropped. `new_workflow` names the workflow that first appeared among the ignored runs since the last match and has run most often since, which is how a workflow renamed after it matched shows up (GitHub and Jenkins monitors; always empty on GitLab, which names an unnamed pipeline after its branch). `last_workflow`, `last_branch`, `same_workflow_last_branch` and `new_workflow` are copied from the CI provider's payload: treat them as untrusted text.
    - `count` integer, required: Number of finished runs the filters ignored.
    - `last_at` string, date-time, required: RFC 3339 UTC time the most recent ignored run arrived.
    - `last_workflow` string, required: Workflow name of the most recent ignored run, as the CI provider sent it.
    - `last_branch` string, required: Branch of the most recent ignored run, as the CI provider sent it.
    - `last_matched_at` string, date-time, nullable, required: RFC 3339 UTC time of the most recent finished run that matched the filters, or null when none has been recorded.
    - `filters_changed_at` string, date-time, nullable, required: RFC 3339 UTC time `ci_workflow` or `ci_branch` last changed, or null when that is not known.
    - `same_workflow_count` integer, required: Number of the ignored finished runs that belong to `ci_workflow` (every ignored run when no workflow filter is set): the runs the branch filter alone dropped. 0 when every ignored run was another workflow's.
    - `same_workflow_last_at` string, date-time, nullable, required: RFC 3339 UTC time the most recent of those runs arrived, or null when `same_workflow_count` is 0.
    - `same_workflow_last_branch` string, required: Branch of the most recent of those runs, as the CI provider sent it; an empty string when `same_workflow_count` is 0.
    - `new_workflow` string, required: Name of the workflow, other than `ci_workflow`, that first appeared after the last matching run and has the most ignored runs since (ties to the one that appeared first), as the CI provider sent it; an empty string when none did. A name first seen in the monitor's first 7 days of recording workflow names is never new. Always empty on GitLab monitors.
    - `new_workflow_first_at` string, date-time, nullable, required: RFC 3339 UTC time `new_workflow` first appeared, or null when `new_workflow_count` is 0.
    - `new_workflow_count` integer, required: Number of the ignored finished runs that carried `new_workflow`; 0 when no new workflow appeared.
  - `runaway_ceiling` integer, nullable: Maximum pings allowed in a rolling 1-hour window. Exceeding it opens a `runaway`-cause incident, which detects a looping agent or job pinging abnormally fast. Omitted from the response when disabled for this monitor.
  - `tags` string[], required: Labels attached to this monitor. Always returned as an array (never null); empty array when no tags are set.
  - `agent_id` string, uuid, nullable: Id of the agent that owns this monitor. Omitted/null when the monitor is unattached — including right after its owning agent was deleted: `DELETE /api/v1/agents/{id}` unowns the monitors it owned, it does not delete them.
  - `source_kind` string: The scanner that discovered this monitor (`crontab`, `github-actions`, `k8s-cronjob`, `systemd-timer`). **Omitted when a human created the monitor**, which is the state of every monitor that predates discovery — and such a monitor is never reconciled, so a scan can neither adopt nor delete it.
  - `source_ref` string: The stable path within `source_kind` that identifies exactly what was found. Together with `source_kind` and the project it is the discovery reconcile key. `source_kind` and `source_ref` are always either both present or both omitted: a half-set identity is never stored.
  - `trace_content` 'redacted' | 'dropped', required: What this monitor's traces keep of prompt, command and tool content. `dropped` (the default) removes the content attributes (prompts, completions, commands, tool arguments, results and definitions, retrieved documents, working directories) of every recognised vocabulary entirely; lengths such as `user_prompt_length`, token counts and cost are kept. Error text (`error`, `error.message`, `exception.message`, `exception.stacktrace` and the span's status message) is kept at `dropped`, redacted and then cut to 512 characters, and a cut marks the span or log event truncated. `redacted` opts in to storing that content, with every secret-shaped value replaced by `[REDACTED:<class>]` at ingest, before anything is queued or stored: API keys (LastPing, Anthropic, OpenAI, GitHub, GitLab, npm, AWS access key ids, Slack, Stripe, Google, Hugging Face, Perplexity, Fireworks, Supabase, New Relic, Airtable, Terraform Cloud, Firebase), HashiCorp Vault tokens, age secret keys, Discord bot tokens, Slack, Discord and Teams webhook URLs, `Authorization` header values, JWTs, private key blocks, `password=`/`token=`/`api_key=` style assignments (subscripted ones such as `os.environ["API_KEY"] = ...` and Dockerfile `ENV`/`ARG` lines included), SQL `PASSWORD '...'` and `IDENTIFIED BY` clauses, the Redis `AUTH` command, `.netrc` passwords, command-line password flags and the argument after them (any secret-named `--flag`, `-p` after a login subcommand, `redis-cli -a`), cookies, credentials in URLs, the value of any attribute or JSON field whose name is secret-named (password, token, api_key, secret, the same word in several other languages, an `Authorization` or `X-Api-Key` header and the like; `Idempotency-Key` and request, correlation and trace ids are not secrets), including JSON carried inside another JSON string such as tool-call arguments, and long high-entropy tokens. A bytes attribute that is valid UTF-8 is scanned and stored as its text. A span that had a value redacted carries `lastping.redacted_values`. A request whose redaction work exceeds a fixed budget stores `[REDACTED:over_budget]` for the values it could not scan, never the values themselves. In both modes every remaining value is redacted the same way. There is no setting that stores content unredacted. A change reaches ingest within 60 seconds: ingest caches each monitor's setting for up to a minute, so an export sent just after the change may still be handled the old way. Always present.
  - `kind` 'cron' | 'ci' | 'agent' | 'http', required: The console's four-way grouping of this monitor, derived server-side so every surface groups it the same way. `agent` wins over the rest: an agent's heartbeat is the agent's. A CI monitor is one with a `ci_provider`, **not** one with `monitor_type: "ci"` — nothing in the product writes that value.
  - `uptime_90d` number, double, nullable, required: Uptime over the last 90 days, or since the monitor was created if that is more recent, as a percentage with two decimals. For an HTTP monitor this is the share of probes that succeeded; for every other type it is derived from incident downtime over the window. Null when an HTTP monitor has no probe history yet, and null for any other monitor that has never been pinged. A monitor created three days ago reports on those three days, not on 90 days of assumed success, so the window varies per monitor and the console says so beside the figure.
  - `latency_ms` integer, nullable, required: The most recent probe's latency. HTTP monitors only; null for every other type and for an HTTP monitor with no probe yet.
  - `routes_count` integer, required: How many distinct alert destinations would hear about this monitor FAILING: the destinations routed on `down`, `fail`, `blocked` or `every-run`. Distinct destinations, not route rows, so one destination routed on both `down` and `fail` is one place the alerts go. `every-run` counts because it fires for every completed run, a failed one included. A route on `recovery`, `success`, `started` or `note` is not counted: routes are resolved per event with no fallback, and none of those four ever fires for a failure, so a monitor routed only on them has nowhere for its bad news to go. 0 means failures would be silent, which the console flags as "Not routed" and Home counts in `overview.alerts.monitors_unrouted`.
  - `recent_outcomes` string[], required: This monitor's last 20 run outcomes, newest first, for the console's run strip. Always an array, empty when the monitor has not completed a run yet. Only terminal outcomes appear: a `start` says a run began and is not one.
  - `hook_version` integer, nullable, required: Version of the Claude Code hook script last seen on a start ping; null when no hook that reports a version has sent a start. Older than hook_version_current means re-run set-up.
  - `hook_version_current` integer, required: The hook script version set-up installs today. Compare it with hook_version.
  - `hook_seen_at` string, date-time, nullable, required: When hook_version was last recorded, from a start ping. Refreshed at most once an hour while the version is unchanged. Null when no hook that reports a version has sent a start.
  - `claude_code_seen_at` string, date-time, nullable, required: Latest time this monitor received a Claude Code run (a hook start that reported a version, or a trace whose source is Claude Code). With hook_version null and a recent value here, the machine runs an older hook or none.

## Other responses

- `400`: Invalid request body or schedule validation failed. For an `http` monitor, `SSRF_BLOCKED_URL` means the `probe_url` host does not resolve to a publicly routable address (it resolves only to private, loopback or otherwise blocked addresses, or it could not be resolved); check the URL and try again.
- `401`: Missing or invalid API key.
- `404`: Check not found in this project.
- `409`: `SOURCE_ALREADY_MONITORED` — another monitor in this project already claims the requested `source_kind`/`source_ref` identity. Two monitors reconciling to one source would make every later discovery run ambiguous, so the PATCH is refused and the monitor is left completely unmodified. `CONCURRENT_UPDATE` — on an `http` monitor, another update changed the stored `probe_url` while this one was in flight. Nothing was changed; retry the request.
- `503`: `MONITOR_BUSY` — the monitor is busy (another update or a ping is in progress) for longer than the request will wait. Nothing was changed; retry after the `Retry-After` interval.

---

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