---
title: WatchFor API errors and rate limits
description: The JSON error envelope and every error code, rate-limit headers and per-plan budgets, Idempotency-Key semantics, and request size and array caps.
canonical: https://watchfor.io/docs/api/errors
---

# WatchFor API errors and rate limits

The JSON error envelope and every error code, rate-limit headers and per-plan budgets, Idempotency-Key semantics, and request size and array caps.

## Error envelope

Errors always return the same JSON shape — on every endpoint, including
unknown paths under `/api/v1/*` (you never get an HTML 404). Switch on the
machine-readable `code`; show the `message` to humans:

```json
{
  "error": {
    "code": "not_found",
    "message": "Monitor not found"
  }
}
```

| HTTP | `code` | When |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed JSON; failed validation (the message names the field and the rule); `limit` outside 1–100 or not an integer; a `sort` parameter (lists have one fixed order); a malformed cursor, or one whose item was deleted ("Cursor no longer valid — restart from the first page"); an unsupported field |
| 401 | `unauthorized` | Missing or malformed `Authorization` header — including a scheme other than `Bearer` ("Unsupported authorization scheme") or empty credentials; unknown, revoked or expired key; invalid or expired OAuth token |
| 403 | `forbidden` | Credential lacks the `write` scope, the plan has no API access, a plan limit was hit (monitor/contact/status-page slots, on-demand checks, advanced report options, a [live diagnostic](/docs/api/diagnostics) the plan excludes), or the OAuth user has no organization |
| 404 | `not_found` | Resource doesn't exist — or belongs to another organization (never distinguished) |
| 405 | `method_not_allowed` | HTTP method not supported on this path; the `Allow` header lists what is |
| 409 | `conflict` | Invalid state transition (acknowledge a resolved incident, run a check on a paused monitor), duplicate (contact email, group name, status-page slug, one rule per metric per severity), a reserved status-page slug, deleting the default contact group or a contact group still in use (without `?force=true`), or an `Idempotency-Key` reused with a different request / still in progress |
| 413 | `payload_too_large` | Request body over the cap — 256 KiB for JSON bodies, 10 KB for an incident false-positive `reason` |
| 429 | `rate_limited` | Per-key request budget exhausted, the plan's on-demand check allowance for that monitor used up, or a [live diagnostics](/docs/api/diagnostics) budget — the per-check plan allowance or the hourly distinct-target ceiling |
| 500 | `internal_error` | Something broke on our side, including a temporarily unavailable metrics backend — safe to retry |
| 501 | `not_implemented` | Reserved for endpoints announced but not yet available |

Validation errors name the field and the rule, joined with `;` when
several fail — the messages list the allowed values so a client (or an AI
agent) can correct the request and retry:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "durationMinutes: Number must be greater than or equal to 5; startsAt: must be a valid ISO 8601 datetime (e.g. 2026-09-01T22:00:00Z)"
  }
}
```

Alert-rule validation is catalog-driven: an unknown metric returns the
valid metrics for that monitor type, a wrong operator returns the valid
operators for that metric, and an out-of-range value returns the allowed
range and unit. See [Alert rules](/docs/api/alert-rules).

## Rate limits

Each credential has a **per-minute request budget** decided by your plan
(a sliding window, shared across all of WatchFor's API replicas). The
budget is applied **per API key**, or per OAuth client + user for OAuth
tokens. Your current budget is visible as `api_requests_per_minute` on
[`GET /v1/plan`](/docs/api/plan); `0` means the plan has no API access and
`-1` means unlimited.

Every response — success or error, a `403` or `404` included — carries
the real remaining count in both the widely used `X-RateLimit-*` headers
and the IETF `RateLimit-*` family:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 60
RateLimit-Limit: 60
RateLimit-Remaining: 42
RateLimit-Reset: 60
```

`Reset` is in seconds. When the budget is exhausted you get `429` with a
`Retry-After` header (seconds):

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded — slow down"
  }
}
```

Back off at least `Retry-After` seconds — ideally with exponential backoff
and jitter — and never retry a `429` immediately. The limit is enforced
*before* the plan gate, so a valid key on a plan without API access is
still bounded.

Notes:

- The [MCP server](/docs/api/mcp) and [A2A](/docs/api/a2a) call the API
  internally with your credential, so their tool and skill calls draw
  from the same budget. `POST /v1/batch` counts each sub-operation.
- Anonymous surfaces — the unauthenticated `401` responses, the public
  spec and catalog, the [sandbox](/docs/api/sandbox), MCP discovery and
  the documentation MCP server — have a separate fixed per-IP anti-abuse
  ceiling and advertise the same header families.
- On-demand checks (`POST /v1/monitors/{id}/check-now`) have their own
  per-monitor hourly allowance set by your plan; exceeding it is also a
  `429` with `Retry-After`.

## Idempotency & safe retries

Every endpoint is safe to retry:

- **GET, PATCH and DELETE** are idempotent by nature — repeating them
  yields the same end state (a repeated `DELETE` returns `404`, which your
  retry logic can treat as success).
- **Action POSTs** (`pause`, `resume`, `acknowledge`, `resolve`,
  `mark-maintenance`) are idempotent no-ops when the resource is already
  in the target state — they return `200` with the current object instead
  of erroring.
- **Creating POSTs** (monitors, bulk actions, alert rules, contacts,
  groups, maintenance windows, status pages, announcements) accept an
  optional **`Idempotency-Key`** header — any unique string up to 255
  characters, typically a UUID.

How `Idempotency-Key` behaves (Stripe semantics):

| Situation | Result |
| --- | --- |
| First request with a key succeeds | Response stored for **24 hours** |
| Same key, same method + path + body within 24 h | Stored response replayed verbatim (same status), with header `Idempotency-Replay: true`; **no duplicate resource** |
| Same key, different method/path/body | `409 conflict` — "already used with a different request" |
| Same key while the first request is still running | `409 conflict` — retry shortly |
| First request failed (validation, plan limit, server error) | Key released — a corrected retry with the same key proceeds normally |
| Key longer than 255 characters | `400 invalid_request` |

Keys are scoped to the credential that sent them. A replay of a stored
`201` is returned even if a plan quota is now full — the quota is full
*because* the first attempt succeeded.

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

## Request limits

| Limit | Value | Error |
| --- | --- | --- |
| JSON request body | 256 KiB (262,144 bytes), enforced while streaming | `413 payload_too_large` |
| Incident false-positive `reason` body | 10 KB | `413 payload_too_large` |
| `limit` query parameter | 1–100, default 50 | `400` for anything else (`0`, `-1`, `abc`, `500`) |
| `sort` query parameter | not supported on any list — each list has one fixed order | `400` |
| `POST /monitors/bulk` ids | 1–100 | `400` |
| `POST /batch` operations | 1–20 | `400` |
| Monitor `locations` | up to 200 ids (plan decides how many may be used) | `400` / `403` |
| Monitor `tags` | up to 10, each 1–32 chars, lowercase `a-z 0-9 - :` | `400` |
| Monitor `name` | 1–50 characters | `400` |
| Contact `groupIds` | up to 50 | `400` |
| Contact group `contactIds` | up to 1,000 | `400` |
| Maintenance window `monitorIds` / `monitorTags` | up to 500 / 50; duration 5–43,200 minutes | `400` |
| Status page `components` | up to 50; each up to 500 `monitorIds` and 20 `monitorTags` | `400` |
| Announcement `title` / `body` | 200 / 5,000 characters | `400` |
| Report-email `recipients` | up to 5 addresses per cadence | `400` |
| Report `monitor_ids` / `tags` query | first 200 / 50 values are used | — |
| Check history `hours` | 1–720 | `400` |

Plan-driven caps (monitors, contacts, groups, status pages, maintenance
windows, on-demand checks, custom report ranges) return `403 forbidden`
with a message naming the limit. Read them ahead of time from
[`GET /v1/plan`](/docs/api/plan).

---

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