---
title: WatchFor maintenance windows API
description: Schedule, adjust and remove maintenance windows from deploy pipelines and change-management tooling — suppress alerts and exclude planned downtime from uptime.
canonical: https://watchfor.io/docs/api/maintenance-windows
---

# WatchFor maintenance windows API

Schedule, adjust and remove maintenance windows from deploy pipelines and change-management tooling — suppress alerts and exclude planned downtime from uptime.

A **maintenance window** suppresses alerts for the covered monitors and
(by default) excludes the time from uptime/SLA while **checks keep
running** — you still see what happened, you just aren't paged and your
numbers aren't dented. Background: [maintenance windows](/docs/monitors/maintenance).

| Method | Path | Scope | operationId |
| --- | --- | --- | --- |
| GET | `/v1/maintenance-windows` | read | `listMaintenanceWindows` |
| POST | `/v1/maintenance-windows` | write | `createMaintenanceWindow` |
| GET | `/v1/maintenance-windows/{id}` | read | `getMaintenanceWindow` |
| PATCH | `/v1/maintenance-windows/{id}` | write | `updateMaintenanceWindow` |
| DELETE | `/v1/maintenance-windows/{id}` | write | `deleteMaintenanceWindow` |

The maintenance window object:

```json
{
  "id": "5f2c…",
  "object": "maintenance_window",
  "name": "DB migration",
  "description": "Quarterly index rebuild",
  "monitor_ids": ["0b0f6a41-…"],
  "monitor_tags": ["prod"],
  "starts_at": "2026-08-30T22:00:00.000Z",
  "duration_minutes": 60,
  "timezone": "UTC",
  "recurrence": "once",
  "recurrence_until": null,
  "suppress_alerts": true,
  "mark_maintenance": true,
  "exclude_from_sla": true,
  "enabled": true,
  "state": "upcoming",
  "next_run_at": "2026-08-30T22:00:00.000Z",
  "auto_publish": false,
  "status_page_ids": [],
  "created_at": "2026-08-24T12:00:00.000Z",
  "updated_at": "2026-08-24T12:00:00.000Z"
}
```

Tag targeting (`monitor_tags`) automatically includes monitors that get
the tag later; `monitor_ids` and `monitor_tags` combine as a union.

| Field | Meaning |
| --- | --- |
| `state` | The dashboard's lifecycle badge: `active` — an occurrence is running right now (alerts muted); `upcoming` — a future occurrence is scheduled; `ended` — nothing left to run; `disabled` — switched off (`enabled: false`) and not currently running. A window switched off while running stays `active` for up to a minute, until its silences are lifted |
| `next_run_at` | Start of the next occurrence — the running one while `active`, the first future slot while `upcoming`; `null` for `ended` and `disabled` |
| `mark_maintenance` | Covered monitors show the Maintenance badge (and `maintenance: true` on [`GET /v1/monitors`](/docs/api/monitors)) while the window runs |
| `auto_publish` / `status_page_ids` | Whether the window posts a "Scheduled maintenance" notice, and to which status pages — configured in the dashboard; read-only here |

## List windows

`GET /v1/maintenance-windows` — latest start time first,
cursor-paginated (`limit` 1–100 — anything else, or a `sort` parameter,
is `400`; `cursor` from the previous page's
`next_cursor`).

```bash
curl https://watchfor.io/api/v1/maintenance-windows \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

`?state=active` or `?state=upcoming` returns only the live windows —
"is anything muted right now?", "what is scheduled?". State is derived
from the schedule rather than stored, so the filter is evaluated over
**all** of the organization's windows before paging: `has_more` and
`next_cursor` describe the filtered set, never just the page. Live
windows are bounded by the plan's window limit, which keeps this cheap.
Any other value is `400`; `ended` and `disabled` are not filterable —
read `state` off each row of the unfiltered list instead.

```bash
curl "https://watchfor.io/api/v1/maintenance-windows?state=active" \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

## Get a window

`GET /v1/maintenance-windows/{id}`

## Create a window

`POST /v1/maintenance-windows` — requires `write` scope. The classic CI
use case: open a window right before a deploy.

| Field | Required | Notes |
| --- | --- | --- |
| `name` | yes | 1–100 characters |
| `startsAt` | yes | ISO 8601 datetime |
| `durationMinutes` | yes | 5–43200 (5 minutes to 30 days) |
| `monitorIds` | at least one of the two | Up to 500 monitor ids; ids outside your organization are silently dropped |
| `monitorTags` | at least one of the two | Up to 50 tags |
| `description` | no | Up to 1000 characters |
| `timezone` | no | IANA name (e.g. `Europe/Vilnius`) used to interpret recurring start times; default `UTC` |
| `recurrence` | no | `once` (default), `daily`, `weekly`, `monthly` |
| `recurrenceUntil` | no | ISO 8601; must be after `startsAt` |
| `suppressAlerts` | no | Default `true` |
| `markMaintenance` | no | Default `true` — show the Maintenance badge on covered monitors while active |
| `excludeFromSla` | no | Default `true` — exclude the time from uptime/SLA |

```bash
curl -X POST https://watchfor.io/api/v1/maintenance-windows \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Idempotency-Key: deploy-v2.14" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Deploy v2.14",
    "startsAt": "2026-08-30T22:00:00Z",
    "durationMinutes": 30,
    "monitorTags": ["prod"]
  }'
```

Returns `201` with the created window. Send an
[`Idempotency-Key`](/docs/api/errors#idempotency--safe-retries) so a CI
job that times out and retries won't open two windows — a replay of the
stored `201` is returned even if the plan's window quota is now full.
The plan's maintenance-window limit applies (`403` when exhausted). Only
**upcoming and active** windows count against it — ended windows are
history and free their slot (the limit per plan is on
[pricing](/pricing)). A window that starts while one of its monitors
is paused still covers that monitor, so a later resume inherits the
suppression and uptime exclusion. If a monitor is still down when its
window ends, the incident's first notification goes out within about a
minute of the window closing.

Publishing a window to status pages (`statusPageIds`, `autoPublish`) is
dashboard-only for now — sending those fields returns `400` rather than
silently dropping them. Announcement-only windows (no monitors) are
therefore also dashboard-only; the API requires at least one monitor id
or tag.

## Update a window

`PATCH /v1/maintenance-windows/{id}` — requires `write` scope. Partial:
`name`, `description`, `startsAt`, `durationMinutes`, `monitorIds`,
`monitorTags`, `suppressAlerts`, `excludeFromSla`, `enabled`.
`enabled: false` disables the window without deleting it (no further
occurrences activate; it reads `state: "disabled"` with a `null`
`next_run_at`). `timezone` and `recurrence` cannot be changed after
creation — delete and recreate the window instead.

```bash
curl -X PATCH https://watchfor.io/api/v1/maintenance-windows/5f2c… \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "durationMinutes": 90 }'
```

## Delete a window

`DELETE /v1/maintenance-windows/{id}` — requires `write` scope. If the
window is active right now its alert silences are lifted immediately;
future occurrences are removed and any status-page maintenance notices
it published are cancelled.

```bash
curl -X DELETE https://watchfor.io/api/v1/maintenance-windows/5f2c… \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

Returns `200` with `{ "object": "deleted", "id": "…", "deleted": true }`.

## Related

- [`POST /v1/incidents/{id}/mark-maintenance`](/docs/api/incidents#mark-an-incident-as-maintenance)
  creates a retroactive window for an outage that already happened.
- [`GET /v1/summary`](/docs/api/summary) reports `open_maintenance_windows` —
  the same count as `?state=active` returns rows.
- MCP tools `list_maintenance_windows`, `create_maintenance_window`,
  `delete_maintenance_window`; A2A skill `manage-maintenance`
  (create / list / cancel).

---

Canonical page: https://watchfor.io/docs/api/maintenance-windows · All docs: https://watchfor.io/docs · Site guide: https://watchfor.io/llms.txt
