# REST conventions

The REST API is at `https://dash.justmonitorit.com/api/v1`. Every operation
an API token can call is in the [REST reference](generated/rest-reference.md);
the machine-readable description is the OpenAPI 3.1 document at
[`/api/v1/openapi.json`](https://dash.justmonitorit.com/api/v1/openapi.json).
In it, operations a token can call list the `api_token` security scheme with
the lowest token role they need, for example
`"security": [{"session": []}, {"api_token": ["member"]}]`.

This page covers what is common to every operation.

## Requests

- Authenticate with `Authorization: Bearer jmi_...` ([Authentication](authentication.md)).
- Request bodies are JSON with `Content-Type: application/json` (anything
  else is `415 unsupported_media_type`), at most 64 KiB
  (`413 payload_too_large`) and nested at most 32 levels deep.
- Field names are `snake_case`. Unknown fields are rejected
  (`422 validation`, field error code `unknown_field`), so a typo never
  passes silently.
- Paths with an organisation start with `/orgs/{org}`; `GET /token` returns
  your token's organisation id.

## Responses

- JSON, `snake_case`. Every field is present; a missing value is `null`, never
  an omitted key.
- **Ids** are prefixed strings: `org_`, `mon_` (monitor), `inc_` (incident),
  `con_` (contact), `ntf_` (notification), `trc_` (path trace), `usr_`
  (user), `inv_` (invitation), `tok_` (API token), followed by 26 characters.
  They sort by creation time. An id with the wrong prefix in a path is
  `404 not_found`.
- **Timestamps** are RFC 3339 in UTC with milliseconds:
  `2026-10-06T12:00:00.000Z`.
- **Durations** are integers with the unit in the name: `interval_s`,
  `timeout_ms`, `duration_s`.
- **Enums** are lowercase `snake_case` strings. New values may be added;
  treat an unknown value as "other".
- Every response has an `X-Request-Id` header and `Cache-Control: no-store`.

Locations are named by their `code` (`lon1`, `fra1`, `nyc1`) wherever a
request takes a location. `GET /locations` lists them and their regions
(`eu`, `na`, `sa`, `as`, `oc`, `af`); it needs no token.

## Errors

Errors are [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) problem
documents with `Content-Type: application/problem+json`:

```json
{
  "type": "https://justmonitorit.com/problems/validation",
  "title": "Validation failed",
  "status": 422,
  "code": "validation",
  "detail": "2 fields are invalid.",
  "request_id": "0192a3c4-7b1e-7c3a-9d2f-5e8a1b2c3d4e",
  "errors": [
    { "pointer": "/spec/url", "code": "target_not_public", "message": "Address resolves to a private network." },
    { "pointer": "/interval_s", "code": "below_plan_minimum", "message": "Your plan allows intervals of 60 s or more." }
  ]
}
```

Switch on `code`, not on `title` or `detail` (those are for people and may
change). `errors` is present only for `422 validation`; each `pointer` is a
JSON Pointer into your request body, or `/query/<name>` for a query
parameter. All invalid fields are reported at once.

| `code` | Status | Meaning |
|---|---|---|
| `bad_request` | 400 | Malformed JSON, JSON nested too deeply, bad query string |
| `unauthenticated` | 401 | No token, an unknown, expired or revoked token, or an endpoint that needs a signed-in person |
| `forbidden` | 403 | The token's effective role is too low, or the action is owner-only or needs a signed-in person |
| `account_suspended` | 403 | The token's creator is suspended |
| `org_suspended` | 403 | The organisation is suspended |
| `org_deleting` | 403 | The organisation is scheduled for deletion |
| `billing_inactive` | 403 | The subscription has ended; reads still work, writes do not |
| `plan_limit` | 403 | The change would exceed the plan; `limit`, `current` and `max` say which and by how much |
| `not_found` | 404 | No such resource in this organisation (also another organisation's id) |
| `conflict` | 409 | The resource is in a state that does not allow this; `detail` says why |
| `contact_unverified` | 409 | A test send to a contact that is not verified yet |
| `contact_disabled` | 409 | A test send to a disabled contact |
| `incident_resolved` | 409 | Acknowledging an incident that is already resolved |
| `sms_allowance_exhausted` | 409 | The organisation's SMS credits for this month are used up |
| `idempotency_in_progress` | 409 | A request with the same `Idempotency-Key` is still running |
| `precondition_failed` | 412 | `If-Match` is stale: someone changed the resource |
| `payload_too_large` | 413 | Body over 64 KiB |
| `unsupported_media_type` | 415 | Body is not `application/json` |
| `validation` | 422 | Invalid fields, listed in `errors` |
| `idempotency_mismatch` | 422 | An `Idempotency-Key` reused with a different body |
| `rate_limited` | 429 | Too many requests; wait `Retry-After` seconds |
| `internal` | 500 | Our bug. It is logged with the request id |
| `not_leader` | 503 | A server failover is in progress; retry after `Retry-After` (2) seconds |
| `unavailable` | 503 | Overloaded or a dependency is down (for example no probe can run a preview check); retry later |

Common field error codes in `errors[].code`: `required`, `too_short`,
`too_long`, `out_of_range`, `invalid_format`, `invalid_id`,
`unknown_field`, `invalid_choice`, `not_allowed`, `duplicate`,
`target_not_public` (the target resolves only to private or reserved
addresses), `host_rate_exceeded` (your monitors would check one host more
than 120 times a minute), `too_few_locations`, `too_many_locations`,
`below_plan_minimum`, `unsupported_tld` (a domain without an RDAP server),
`phone_country_not_allowed`, `phone_not_mobile`, `phone_not_callable` (a
premium-rate, toll-free, shared-cost, personal or VoIP number given for a
phone call contact), `discord_webhook_invalid`,
`pushover_invalid`, `pagerduty_key_invalid`, `slack_webhook_invalid`,
`teams_webhook_invalid`, `teams_webhook_retired` (an Office 365 connector
URL, which Microsoft retired in May 2026: use a Workflows webhook),
`webhook_url_invalid`, `webhook_domain_unverified`.

**Retrying.** Retry `429` after `Retry-After`, and `503` after
`Retry-After` (or a few seconds) with backoff. Do not retry other `4xx`
responses unchanged. A `POST` that creates something is safe to retry only
with an `Idempotency-Key` (below).

## Pagination

Lists that can grow are paginated with cursors:

```http
GET /orgs/{org}/incidents?limit=100&cursor=djE6WyIyMDI2...
```

```json
{ "data": [ ... ], "next_cursor": "djE6WyIyMDI2..." }
```

Pass `next_cursor` as `cursor` to get the next page; it is `null` on the last
page. `limit` defaults to 50 and is at most 200, except monitors (500) and
check results (1000). Cursors are opaque: do not build or change them. Keep
the other query parameters the same between pages. Lists stay stable while
new items are added.

Short lists (members, a monitor's contacts, status timelines) return
`{"data": [...]}` without a cursor.

## Idempotency keys

`POST` requests that create something accept an `Idempotency-Key` header:
creating a monitor, a contact or an invitation, and sending a test
notification. The key is 8-64 characters of `A-Z a-z 0-9 _ -`; use a new
random value (a UUID) for each intended creation.

- Retrying with the same key and the same body returns the stored response
  (with `Idempotent-Replayed: true`) instead of creating a duplicate.
- The same key with a different body is `422 idempotency_mismatch`.
- While the first request is still running, a retry gets
  `409 idempotency_in_progress`.

Keys are remembered for 24 hours. They belong to you, so your tokens and
dashboard sessions share the key space.

## Concurrency: `ETag` and `If-Match`

Monitors and contacts have a `version` that increases with every change.
Their responses carry `ETag: "<version>"`. Send it back on `PATCH`, `PUT`
or `DELETE`:

```http
PATCH /orgs/{org}/monitors/{mon}
If-Match: "7"
Content-Type: application/json

{"interval_s": 300}
```

If someone changed the monitor since version 7, the answer is
`412 precondition_failed` and nothing is written: fetch it again and decide.
Without `If-Match` the change is applied to the latest version.

## Updates (`PATCH`)

`PATCH` uses JSON Merge Patch ([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396))
with `Content-Type: application/json`:

- a field you omit is unchanged;
- `null` clears a field that may be null;
- arrays are replaced whole (send the complete list of `labels` or
  `contact_ids`);
- a monitor's `spec` is merged field by field, so
  `{"spec": {"timeout_ms": 5000}}` changes only the timeout.

A monitor's or contact's `kind` cannot change (`422`, code `not_allowed`).

## Rate limits

| What | Limit |
|---|---|
| Reads (`GET`) | 600 a minute per token |
| Writes | 120 a minute per user (all their tokens and sessions) and 120 a minute per organisation |
| Rejected tokens | 30 a minute per IP address; beyond that, requests with a bad token are `429` |
| Preview checks (`POST .../monitors/preview-check`) | 10 a minute per organisation |
| Path traces (`POST .../monitors/{mon}/traces`) | 10 a minute per organisation |
| Invitations (`POST .../invites`) | 20 a day per organisation |
| Verification codes (`POST .../contacts/{con}/verify/send`) | 3 an hour per contact and 10 a day per organisation; SMS and phone calls together 3 a day per phone number, email 5 a day per address |
| Verifying a code | 5 attempts per code, then the code is void |
| Test notifications (`POST .../contacts/{con}/test`) | 5 per 10 minutes per contact and 30 an hour per organisation |

Over a limit the answer is `429 rate_limited` with `Retry-After` in seconds.
GraphQL queries and MCP tool calls count as the REST requests they make.

## Plan limits

What an organisation may do depends on its plan: the number of monitors and
contacts, the shortest interval, locations per monitor, checks per minute,
seats, SMS credits and how long results are kept. `GET
/orgs/{org}/entitlements` returns the limits and the current usage. A change
that would exceed one is `403 plan_limit` with the `limit` that is hit, the
`current` usage and the `max`. Results and history older than the plan's
retention are not returned.

## Versioning

The major version is in the path (`/api/v1`). We add endpoints, response
fields and enum values without a new version, so ignore fields you do not
know and handle unknown enum values. A breaking change would ship as
`/api/v2` beside v1. Every response carries `X-JMI-Build`, the
server build.
