# Bulk operations on monitors

Once you have more than a few monitors, change them together. Every bulk change - in the app or through the API -
runs as a background **job**: it is accepted at once, applies to each monitor separately, and reports a result
per monitor. One monitor that cannot be changed never blocks the others.

## Select and act in the app

1. On **Sites**, tick the checkboxes of the monitors you want. **Select all** ticks the current page; click it
   again (**Select all N**) to select every monitor that matches your current filters, across pages.
2. The bulk bar appears at the bottom. Choose an action:

| Action | What it does |
|---|---|
| **Edit** | Opens **Edit N monitors** (below). With exactly one monitor selected, opens that monitor's own editor instead. |
| **Report** | Builds an uptime report for the selection. See [Uptime reports](/reports/uptime-reports/). |
| **Enable** / **Disable** | Resumes or pauses every selected monitor. An all-enabled selection shows only **Disable**, an all-paused one only **Enable**, a mixed one both. |
| **Reset Stats** | Clears the uptime and state history of the selection ("This deletes the computed uptime and state history. Monitoring continues and starts with fresh statistics."). |
| **Delete** | Deletes the selection after a confirmation that shows how many monitors the server will delete and a sample of them. |

### The Edit N monitors panel

Every field starts at **No change**; only what you set is applied, and each monitor keeps everything else.

| Field | Values | Notes |
|---|---|---|
| **Monitoring interval** | **No change** or an interval | One interval for every selected monitor. Monitors whose type has a higher minimum are listed in a note. |
| **Tags to add** / **Tags to remove** | Tags | Adds or removes tags; each monitor keeps its other tags. |
| **Full logging** | No change / On / Off | On is disabled if your plan does not include Full Log. |
| **Open statistics** | No change / On / Off | The public statistics page. |
| **Monitoring state** | No change / Enabled / Paused | Pause or resume the selection. |
| **Monitoring Locations** and **If selected locations are unavailable** | Location tree; No change / Selected locations only / Closest locations / Any location | Shown only when the selection can share one location tree: any mix of web, API, ping and port monitors, or monitors of **one** browser type (Page speed, Transaction or Web content check). |

Click **Update**. The panel shows the job's progress and then the per-monitor result; you can close it while the
job runs.

### Follow the job

Running and finished jobs appear in the jobs indicator in the top bar and in the notification bell, whichever page
you are on. A finished job shows how many monitors were updated, created disabled, skipped or failed, with the
reason for each failure.

## How a job ends

| Job `state` | Meaning |
|---|---|
| `queued`, `running` | Still working. |
| `succeeded` | Every item was applied. |
| `partial` | Some items were applied and some failed. This is a normal outcome, not an error - read the per-item results. |
| `failed` | The job itself could not run, or every item failed. |
| `cancelled` | Stopped on request; items already applied stay applied. |
| `interrupted` | The server running it stopped; it keeps what it applied and can be resumed. |

Each item has a `status`: `created`, `createdDisabled` (created, but disabled because your plan's monitor limit was
reached), `updated`, `skipped`, `failed` (with an error), `deleted`, `cancelled` (not run because the job stopped),
or `pending`.

**Over your plan's limit.** The app always runs bulk jobs so that the batch continues past failures and anything
that would exceed your monitor limit lands **disabled** instead of failing: enabling 20 monitors with room for 15
enables 15 and leaves 5 disabled. The API does the same only when you ask for it (`onOverlimit: "disable"`); its
default is to fail those items.

## Do it with the API or MCP

All bulk calls need scope `monitor:write`, answer `202` with a job id, and require an `Idempotency-Key` header, so a
retried request never starts a second job.

| Operation | Endpoint | Body |
|---|---|---|
| Preview a create batch | `POST /monitor/bulk-validate` | Same as the create; nothing is written |
| Create many monitors | `POST /monitor/bulk` | `{ "defaults": {...}, "items": [ {...}, ... ] }` |
| Preview which monitors an edit touches | `POST /monitor/bulk-update-validate` | `{ "ids": [...] }` or `{ "filter": {...} }`, plus `patch` |
| Edit (or reset statistics of) many monitors | `POST /monitor/bulk-update` | `{ "ids": [...] or "filter": {...}, "patch": {...}, "operation": "resetStats" }` |
| Preview a delete | `POST /monitor/bulk-delete-validate` | `{ "filter": {...} }` -> `{ matched, sample, truncated, max }` |
| Delete many monitors | `POST /monitor/bulk-delete` | `{ "filter": {...}, "expectedCount": <matched> }` |
| Reset one monitor's statistics | `POST /monitor/{id}/reset-stats` | none |
| Poll a job | `GET /job/{id}` | - |
| List recent jobs | `GET /job?kind=...&state=...` | - |
| Cancel / resume | `POST /job/{id}/cancel`, `POST /job/{id}/resume` | - |

Pause every monitor tagged `staging`:

```bash
curl -X POST https://api2.host-tracker.com/monitor/bulk-update \
  -H "Authorization: Bearer $HT_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: pause-staging-2026-09-29" \
  -d '{ "filter": { "tags": ["staging"] }, "patch": { "enabled": false } }'
# 202 { "jobId": "...", "accepted": 12 }   + Retry-After: seconds before the first poll

curl "https://api2.host-tracker.com/job/$JOB_ID" -H "Authorization: Bearer $HT_TOKEN"
# { "state": "succeeded", "progress": { "done": 12, "total": 12 },
#   "summary": { "created": 0, "updated": 12, "skipped": 0, "failed": 0, "deleted": 0 },
#   "results": [ { "index": 0, "status": "updated", "entityId": "...", "result": { ...the monitor... } }, ... ],
#   "nextCursor": null, "hasMore": false }
```

**Targets.** Name them with `ids` (a list of monitor ids) or a `filter` object - never both. The filter members are
`monitorIds`, `types`, `tags` (any of), `states` (`up`, `down`, `paused`, `maintenance`), `enabled` and `q` (text
search), combined with AND. A filter that narrows by nothing is refused (`422 filter_required`) rather than taken to
mean your whole account.

**The patch** is the same body as a single `PATCH /monitor/{id}`: `interval`, `enabled`, `fullLog`, `openStat`,
`addTags` / `removeTags` (or `tags` to replace), `locations`, `recheck`, `settings`, `slaTarget`...

**Policy members** (every bulk job):

| Member | Values | Default |
|---|---|---|
| `onError` (alias `continueOnError: true/false`) | `continue`, `stop` | `continue` - one bad item costs one item |
| `onOverlimit` | `fail`, `disable`, `stop` | `fail` - the item fails with `package_limit`; `disable` creates or leaves it disabled |
| `onDuplicate` (create only) | `fail`, `skip`, `createAnyway` | `fail` - an item whose type and address already exist fails with `duplicate_monitor` |
| `callback` | `{ "webhookId": "...", "on": "completed" or "progress" }` | none - calls your [webhook](/integrations/webhooks/) with `job.completed` (and `job.progress` if asked) |

**Deleting safely.** Run `bulk-delete-validate`, show the `matched` count and `sample` to a person, then send the
same filter with `expectedCount` set to that count. If the selection changed in between, the delete is refused with
`409 selection_mismatch` - validate again.

**Results.** `GET /job/{id}` pages its `results` with `limit` and `cursor`; each item carries the full monitor as it
stands now (or the deletion receipt), or a problem document in `error`. A job is readable for 7 days
(`expiresAt`), then answers `404`.

**MCP.** **`bulk_create_monitors`**, **`bulk_update_monitors`** and **`bulk_delete_monitors`** run the matching
validate call first and only submit on a second call (`submit=true`, or `confirmed=true` plus `expectedCount` for
the delete). Then **`wait_for_job`** (polls for up to about 30 seconds per call) or **`get_job`**, and
**`cancel_job`** / **`resume_job`**. Intervals in `patchJson` and `defaultsJson` are in **seconds**
(`{"interval":300}` = 5 minutes).

## Limits and gotchas

- **Size.** A create batch carries at most your plan's monitor count, never more than 5000 items
  (`GET /account` -> `limits.maxBulkItems`). An update or delete targets at most 2000 monitors
  (`limits.maxBulkSelection`). Larger requests are refused with `422 too_many_items`.
- **Mixed types.** One patch goes to every monitor, so an interval below one type's minimum fails for those
  monitors (`interval_below_type_floor`) while the rest are updated. Locations sent to types without a location picker
  (Database, SNMP, Counter, the expiry, DNSBL and Web Risk checks) fail for those monitors.
- **Cancel is not undo.** Items processed before the cancel stay changed.
- A job whose state is `failed` still answers `200` on `GET /job/{id}`; check `state`, not the HTTP status.

## Related

- [Tag and organize your monitors](/monitors/tags/)
- [Pause, enable and delete a monitor](/monitors/pause-enable-delete/)
- [Importing a list of monitors](/monitors/importing/)
- [REST API v2](/integrations/rest-api/)
