# JustMonitorIt API > JustMonitorIt checks websites, APIs, servers, certificates, domains and mail servers from several locations and alerts by email, SMS, phone call, Pushover, Discord, PagerDuty, Slack, Microsoft Teams and webhooks. Its API comes in three forms over the same operations: REST (JSON), GraphQL and an MCP server for AI assistants. All three authenticate with an organisation-scoped API token: `Authorization: Bearer jmi_…`. - REST base URL: `https://dash.justmonitorit.com/api/v1`; organisation paths are `/orgs/{org}/...`, and `GET /api/v1/token` returns the token's organisation id. - GraphQL: `POST https://dash.justmonitorit.com/api/v1/graphql`. - MCP (Streamable HTTP, stateless): `https://dash.justmonitorit.com/api/v1/mcp`. - Token roles are `viewer` (read), `member` (also change monitors, incidents, contacts) and `admin` (also members, invitations, settings, audit log). - Errors are RFC 7807 problem documents; switch on their `code`. ## Docs - [Overview](https://dash.justmonitorit.com/docs/api/index.md): The three APIs (REST, GraphQL, MCP), the base URL, a three-step quick start and the main concepts - [Authentication](https://dash.justmonitorit.com/docs/api/authentication.md): API tokens: creating them, roles (viewer, member, admin), effective role, expiry, revocation and what tokens cannot do - [REST conventions](https://dash.justmonitorit.com/docs/api/rest.md): JSON, ids, timestamps, error codes, pagination, idempotency keys, If-Match, PATCH semantics, rate limits and plan limits - [Monitor kinds](https://dash.justmonitorit.com/docs/api/monitor-kinds.md): The fields, defaults and limits of a monitor and of the spec of each kind: http, http_adv, tcp, icmp, ssl_expiry, domain_expiry, imap, smtp - [Webhooks](https://dash.justmonitorit.com/docs/api/webhooks.md): Webhook contacts: verifying your domain with a DNS TXT record, the JSON we post (version 1), and how your response is handled - [Examples](https://dash.justmonitorit.com/docs/api/examples.md): Recipes in curl, Python and JavaScript: list down monitors, create monitors, pause during deploys, acknowledge incidents, export uptime, attach contacts - [Terraform and Pulumi](https://dash.justmonitorit.com/docs/api/terraform.md): Managing monitors and contacts with the Terraform, OpenTofu and Pulumi provider: installing it from dash.justmonitorit.com, monitors, contacts, secrets, drift, import and limits - [GraphQL](https://dash.justmonitorit.com/docs/api/graphql.md): The GraphQL API: endpoint, authentication, queries, mutations, errors and limits - [MCP server](https://dash.justmonitorit.com/docs/api/mcp.md): Connecting an AI assistant to the JustMonitorIt MCP server (Streamable HTTP, stateless) with an API token ## Optional - [REST reference](https://dash.justmonitorit.com/docs/api/generated/rest-reference.md): Every REST operation an API token can call, with its minimum role, parameters, request body, responses and errors, and the schemas - [MCP tools](https://dash.justmonitorit.com/docs/api/generated/mcp-tools.md): Every tool of the MCP server with its input schema and the token role it needs - [schema.graphql](https://dash.justmonitorit.com/docs/api/generated/schema.graphql): The GraphQL schema (SDL) - [OpenAPI document](https://dash.justmonitorit.com/api/v1/openapi.json): the machine-readable REST description (OpenAPI 3.1) --- # JustMonitorIt API Everything you do in the JustMonitorIt dashboard you can also do from your own code: create and change monitors, read results and uptime, acknowledge incidents, manage contacts. There are three ways in, all over HTTPS and all authenticated with the same API tokens: | Surface | Endpoint | Use it for | |---|---|---| | [REST](rest.md) | `https://dash.justmonitorit.com/api/v1` | Scripts, CI pipelines, infrastructure as code. JSON over HTTP; an [OpenAPI 3.1 document](https://dash.justmonitorit.com/api/v1/openapi.json) describes every operation. | | [GraphQL](graphql.md) | `https://dash.justmonitorit.com/api/v1/graphql` | Dashboards and tools that want several related things (monitors with their uptime and open incidents) in one request. | | [MCP](mcp.md) | `https://dash.justmonitorit.com/api/v1/mcp` | AI assistants (Claude, Cursor, VS Code and other MCP clients) that should look at your monitors and incidents, or change them. | GraphQL and MCP run the same operations as REST, so the same roles, validation, plan limits, rate limits and audit log apply whichever you use. ## Quick start 1. **Create a token.** In the dashboard, open **Settings → API tokens**, choose a name, a role (`viewer` to read, `member` to also change monitors, incidents and contacts) and an expiry, and copy the token. It starts with `jmi_` and is shown only once. See [Authentication](authentication.md). 2. **Check it works.** `GET /token` describes the token, including the id of its organisation: ```sh export JMI_TOKEN=jmi_... # your token curl -s https://dash.justmonitorit.com/api/v1/token \ -H "Authorization: Bearer $JMI_TOKEN" ``` ```json { "token": { "id": "tok_01j9...", "name": "CI", "role": "member", "prefix": "Ab3dE9xY", "...": "..." }, "org": { "id": "org_01j9...", "name": "Acme" }, "effective_role": "member" } ``` 3. **Call the API.** Everything belonging to your organisation is under `/orgs/{org}`. List the monitors that are down: ```sh ORG=org_01j9... # from step 2 curl -s "https://dash.justmonitorit.com/api/v1/orgs/$ORG/monitors?status=down" \ -H "Authorization: Bearer $JMI_TOKEN" ``` More in [Examples](examples.md): creating monitors, pausing them during a deploy, acknowledging incidents and exporting uptime, in curl, Python and JavaScript. ## Pages - [Authentication](authentication.md): API tokens, roles, expiry and revocation. - [REST conventions](rest.md): JSON, ids, errors, pagination, idempotency, concurrency and rate limits. - [REST reference](generated/rest-reference.md): every operation a token can call, with parameters, bodies and responses. - [Monitor kinds](monitor-kinds.md): the `spec` of each of the eight monitor kinds. - [Examples](examples.md): complete recipes. - [Terraform and Pulumi](terraform.md): monitors and contacts as code, with Terraform, OpenTofu or Pulumi. - [GraphQL](graphql.md): the schema, queries and mutations. - [MCP server](mcp.md): connecting an AI assistant. For language models, [`/llms.txt`](https://dash.justmonitorit.com/llms.txt) lists these pages as Markdown and [`/llms-full.txt`](https://dash.justmonitorit.com/llms-full.txt) has all of them in one file. ## Concepts - **Organisation.** Monitors, contacts and incidents belong to an organisation. A token works on one organisation only. - **Monitor.** One check of one target (an HTTP URL, a host, a port, a certificate, a domain, a mail server), run every `interval_s` seconds from at least two **locations** (`lon1`, `fra1`, `nyc1`, ...; `GET /locations` lists them). A monitor's status is `up`, `down`, `degraded`, `no_data`, `unknown` or `paused`. It only goes down when failures are confirmed from a quorum of locations by more than one probe, so one bad network path does not page you. - **Incident.** Opened when a monitor goes down or degraded, resolved when it recovers. It records a timeline (status changes, notifications, acknowledgements, notes) and the cause. - **Contact.** Where notifications go: an email address, a mobile number (SMS), a phone number we call and read the alert to (a mobile or a landline; a call uses 2 SMS credits, 4 outside the UK, Ireland, the US and Canada, and more for numbers that cost more to call), a Pushover user, a Discord webhook, a PagerDuty service (an Events API v2 integration key: down opens a PagerDuty incident and the recovery resolves it), a Slack channel (an incoming webhook), a Microsoft Teams channel (a Workflows webhook, `secret.webhook_url`) or a webhook (JSON posted to your own HTTPS endpoint). Email, SMS, phone call and Teams contacts are verified with a code (a phone call contact gets a call that reads it out; a Teams contact gets it posted to the channel) before they receive alerts. A webhook is sent nothing until its host has the TXT record shown on the contact (`config.txt_name`, `config.txt_value`), which proves you control the domain; then "Check again" (`verify/send`) activates it. ## Support Questions and problems: support@justmonitorit.com. Include the `X-Request-Id` header of the response (also `request_id` in error bodies); it identifies the request in our logs. --- # Authentication The REST API, the GraphQL API and the MCP server all authenticate with an **API token** sent in the `Authorization` header: ```http Authorization: Bearer jmi_Ab3dE9xY... ``` A token is 47 characters: `jmi_` followed by 43 random characters. Tokens in query strings or cookies are not accepted. ## Creating a token Open **Settings → API tokens** in the dashboard and choose: - **Name**: what the token is for ("Terraform", "Status board", "Claude"), 1-64 characters. It appears in the token list and in the audit log. - **Role**: `viewer`, `member` or `admin` (below), at most your own role in the organisation. - **Expiry**: 1 to 365 days, or never. The dashboard suggests 90 days. Creating a token asks for your password again if you have not entered it in the last 10 minutes. The token is shown **once**; copy it into your secret store straight away. JustMonitorIt keeps only a hash of it and cannot show it again. The list keeps the first 8 characters after `jmi_` (the **prefix**) so you can tell tokens apart, and shows when and from which address each one was last used. An organisation can have at most 50 tokens. ## Whose token it is A token belongs to **you and one organisation**. It works only on that organisation's paths (`/orgs/{org}/...`); with any other organisation's id the API answers `404 not_found`, as it would to a non-member. `GET /token` tells a program which organisation its token belongs to. The token acts for you, so: - Changes made with it are recorded in the audit log as yours, with `data.api_token` naming the token (`{"id": "tok_...", "name": "..."}`). Acknowledging an incident with a token shows you as the acknowledger. - Its **effective role** is the lower of the token's role and your current role in the organisation, checked on every request. - If your role is lowered below a token's role, that token is revoked (the audit log records `api_token.revoked` with reason `creator_role_lost`). - If you leave or are removed from the organisation, all your tokens for it are deleted. - If your account is suspended, your tokens stop working at once (`403 account_suspended`). ## Roles | Action | `viewer` | `member` | `admin` | |---|---|---|---| | Read monitors, results, uptime, incidents, contacts, notifications, plan and usage | yes | yes | yes | | Create, change, pause, resume and delete monitors; run checks and path traces | | yes | yes | | Acknowledge incidents and add notes | | yes | yes | | Create, change, verify, test and delete contacts | | yes | yes | | Rename the organisation, change its timezone | | | yes | | Invite people (not as owner), change members' roles (not to or from owner), remove members (not owners) | | | yes | | Read the audit log | | | yes | The [REST reference](generated/rest-reference.md) lists the lowest token role of every operation. A token whose role is too low gets `403 forbidden`. ### What a token can never do Some actions need a person signed in to the dashboard: - **Owner-only actions**: subscribing and the billing portal, exporting the organisation's data, deleting the organisation, granting or removing ownership, inviting an owner. There are no owner tokens, so these answer `403 forbidden`. - **Actions that ask for your password again**, such as re-enabling an email contact that bounced: `403 forbidden` with the detail "This action needs a signed-in session; API tokens cannot perform it." - **Account and session endpoints**: managing tokens themselves, your profile, passwords and two-factor settings, the list of your organisations, accepting invitations and the dashboard's live event stream. With a token these answer `401 unauthenticated`. ## Expiry and revocation An expired or revoked token gets `401 unauthenticated` with ```http WWW-Authenticate: Bearer realm="JustMonitorIt", error="invalid_token" ``` Revoke a token in **Settings → API tokens**. You can revoke your own tokens; owners and admins can revoke anyone's. Revocation takes effect on the next request. From one IP address, more than 30 rejected tokens a minute are answered with `429 rate_limited` (with `Retry-After`) without checking the token. ## Rate limits Each token may make 600 reads (`GET`) a minute. Writes count against 120 a minute for you (shared with your dashboard sessions and your other tokens) and 120 a minute for the organisation. Some operations have their own limits, listed in [REST conventions](rest.md#rate-limits). A request over a limit gets `429 rate_limited` with a `Retry-After` header in seconds. ## Keeping tokens safe - Use one token per program or integration, so you can revoke one without breaking the others. - Give each token the lowest role that works: `viewer` for dashboards and reports. - Set an expiry, and rotate tokens by creating the new one before revoking the old one. - Keep tokens in a secret manager or your CI's secret store, never in source control. If one leaks, revoke it in the dashboard; the audit log shows what it did. - The API is for servers and scripts. It sends no CORS headers, so a browser page on another site cannot call it; do not put a token in front-end code. --- # 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/` 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: ""`. 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. --- # REST API reference Generated from the API's OpenAPI document ([`/api/v1/openapi.json`](https://dash.justmonitorit.com/api/v1/openapi.json)). Every path below is relative to `https://dash.justmonitorit.com/api/v1`. Send your API token as `Authorization: Bearer jmi_…`; the token role shown for each operation is the lowest role that may call it. Conventions (errors, pagination, idempotency, `If-Match`, rate limits) are in [REST conventions](../rest.md). ## Contents - **Token** - [Describe the calling token](#describe-the-calling-token) `GET /token` - **Organisation** - [Get the organisation](#get-the-organisation) `GET /orgs/{org}` - [Update the organisation](#update-the-organisation) `PATCH /orgs/{org}` - [Audit log](#audit-log) `GET /orgs/{org}/audit-log` - [Plan limits and usage](#plan-limits-and-usage) `GET /orgs/{org}/entitlements` - [List open invitations](#list-open-invitations) `GET /orgs/{org}/invites` - [Invite someone](#invite-someone) `POST /orgs/{org}/invites` - [Revoke an invitation](#revoke-an-invitation) `DELETE /orgs/{org}/invites/{inv}` - [Resend an invitation](#resend-an-invitation) `POST /orgs/{org}/invites/{inv}/resend` - [List members](#list-members) `GET /orgs/{org}/members` - [Change a member's role](#change-a-member-s-role) `PATCH /orgs/{org}/members/{usr}` - [Remove a member](#remove-a-member) `DELETE /orgs/{org}/members/{usr}` - [Organisation overview](#organisation-overview) `GET /orgs/{org}/overview` - **Monitors** - [List locations](#list-locations) `GET /locations` - [List monitors](#list-monitors) `GET /orgs/{org}/monitors` - [Create a monitor](#create-a-monitor) `POST /orgs/{org}/monitors` - [Run a check once](#run-a-check-once) `POST /orgs/{org}/monitors/preview-check` - [Get a monitor](#get-a-monitor) `GET /orgs/{org}/monitors/{mon}` - [Update a monitor](#update-a-monitor) `PATCH /orgs/{org}/monitors/{mon}` - [Delete a monitor](#delete-a-monitor) `DELETE /orgs/{org}/monitors/{mon}` - [List a monitor's contacts](#list-a-monitor-s-contacts) `GET /orgs/{org}/monitors/{mon}/contacts` - [Set a monitor's contacts](#set-a-monitor-s-contacts) `PUT /orgs/{org}/monitors/{mon}/contacts` - [List a monitor's incidents](#list-a-monitor-s-incidents) `GET /orgs/{org}/monitors/{mon}/incidents` - [Pause a monitor](#pause-a-monitor) `POST /orgs/{org}/monitors/{mon}/pause` - [List heartbeat pings](#list-heartbeat-pings) `GET /orgs/{org}/monitors/{mon}/pings` - [List check results](#list-check-results) `GET /orgs/{org}/monitors/{mon}/results` - [Resume a monitor](#resume-a-monitor) `POST /orgs/{org}/monitors/{mon}/resume` - [List rollups](#list-rollups) `GET /orgs/{org}/monitors/{mon}/rollups` - [Response-time series](#response-time-series) `GET /orgs/{org}/monitors/{mon}/series` - [Status timeline](#status-timeline) `GET /orgs/{org}/monitors/{mon}/timeline` - [List path traces](#list-path-traces) `GET /orgs/{org}/monitors/{mon}/traces` - [Run a path trace](#run-a-path-trace) `POST /orgs/{org}/monitors/{mon}/traces` - [Uptime](#uptime) `GET /orgs/{org}/monitors/{mon}/uptime` - [Get a path trace](#get-a-path-trace) `GET /orgs/{org}/traces/{trc}` - **Incidents** - [List incidents](#list-incidents) `GET /orgs/{org}/incidents` - [Get an incident](#get-an-incident) `GET /orgs/{org}/incidents/{inc}` - [Acknowledge an incident](#acknowledge-an-incident) `POST /orgs/{org}/incidents/{inc}/acknowledge` - [Add a note to an incident](#add-a-note-to-an-incident) `POST /orgs/{org}/incidents/{inc}/notes` - **Contacts** - [List contacts](#list-contacts) `GET /orgs/{org}/contacts` - [Create a contact](#create-a-contact) `POST /orgs/{org}/contacts` - [Get a contact](#get-a-contact) `GET /orgs/{org}/contacts/{con}` - [Update a contact](#update-a-contact) `PATCH /orgs/{org}/contacts/{con}` - [Delete a contact](#delete-a-contact) `DELETE /orgs/{org}/contacts/{con}` - [Disable a contact](#disable-a-contact) `POST /orgs/{org}/contacts/{con}/disable` - [Enable a contact](#enable-a-contact) `POST /orgs/{org}/contacts/{con}/enable` - [Set a contact's monitors](#set-a-contact-s-monitors) `PUT /orgs/{org}/contacts/{con}/monitors` - [Send a test notification](#send-a-test-notification) `POST /orgs/{org}/contacts/{con}/test` - [Verify a contact](#verify-a-contact) `POST /orgs/{org}/contacts/{con}/verify` - [Send a verification code](#send-a-verification-code) `POST /orgs/{org}/contacts/{con}/verify/send` - **Notifications** - [List notifications](#list-notifications) `GET /orgs/{org}/notifications` - [Get a notification](#get-a-notification) `GET /orgs/{org}/notifications/{ntf}` - **Maintenance windows** - [List maintenance windows](#list-maintenance-windows) `GET /orgs/{org}/maintenance` - [Schedule a maintenance window](#schedule-a-maintenance-window) `POST /orgs/{org}/maintenance` - [Get a maintenance window](#get-a-maintenance-window) `GET /orgs/{org}/maintenance/{mnt}` - [Change a maintenance window](#change-a-maintenance-window) `PATCH /orgs/{org}/maintenance/{mnt}` - [Delete a maintenance window](#delete-a-maintenance-window) `DELETE /orgs/{org}/maintenance/{mnt}` - [End a maintenance window now](#end-a-maintenance-window-now) `POST /orgs/{org}/maintenance/{mnt}/end` - **Billing** - [List plans](#list-plans) `GET /billing/plans` - [Billing state](#billing-state) `GET /orgs/{org}/billing` - [Schemas](#schemas) ## Token ### Describe the calling token `GET /token` · token role: `viewer` The token making the request, the organisation it belongs to and its effective role (the lower of the token's role and its creator's current role). Use it to find the org id for the other paths. Only API tokens can call it. **Responses** - `200`: [`TokenInfo`](#schema-tokeninfo) - Errors: `401` `unauthenticated` (no, or an invalid, API token); `403` `account_suspended`; `429` `rate_limited` ## Organisation ### Get the organisation `GET /orgs/{org}` · token role: `viewer` Name, timezone, state, your effective role (`my_role`) and plan of the organisation. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Responses** - `200`: [`Org`](#schema-org) - Errors: `401` `unauthenticated`; `403` `account_suspended`; `404` `not_found`; `429` `rate_limited` ### Update the organisation `PATCH /orgs/{org}` · token role: `admin` Rename the organisation or change its timezone (an IANA name such as `Europe/London`). **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Request body** ([`OrgPatch`](#schema-orgpatch)) | Field | Type | Description | |---|---|---| | `name` (optional) | string or null | 1-100 characters. | | `timezone` (optional) | string or null | IANA timezone. | **Responses** - `200`: [`Org`](#schema-org) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `billing_inactive`; `404` `not_found`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### Audit log `GET /orgs/{org}/audit-log` · token role: `admin` Who did what in the organisation, newest first. Entries made with an API token carry `data.api_token` (`{id, name}`). **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Query parameters** | Name | Type | Description | |---|---|---| | `action` (optional) | string | Exact action, e.g. `monitor.updated` (at most 64 characters). | | `actor` (optional) | string | Actor user id (`usr_…`). | | `cursor` (optional) | string | Opaque cursor from `next_cursor`. | | `limit` (optional) | integer | 1-200, default 50. | **Responses** - `200`: page of `AuditEntry` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation` (`/query/...`); `429` `rate_limited` ### Plan limits and usage `GET /orgs/{org}/entitlements` · token role: `viewer` What the organisation may do right now (monitor count, minimum interval, locations per monitor, checks per minute, contacts, seats, SMS credits, retention) and how much of it is used. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | string | Organisation id (`org_…`); `GET /token` returns it | **Responses** - `200`: [`OrgEntitlements`](#schema-orgentitlements) - Errors: `401` `unauthenticated`; `403` `account_suspended`, `org_deleting`; `404` `not_found` (unknown org or not a member); `429` `rate_limited` ### List open invitations `GET /orgs/{org}/invites` · token role: `admin` Invitations that have not been accepted, revoked or expired, newest first. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Query parameters** | Name | Type | Description | |---|---|---| | `cursor` (optional) | string | Opaque cursor from `next_cursor`. | | `limit` (optional) | integer | 1-200, default 50. | **Responses** - `200`: page of [`Invite`](#schema-invite) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation` (`/query/cursor`, `/query/limit`); `429` `rate_limited` ### Invite someone `POST /orgs/{org}/invites` · token role: `admin` Email an invitation to join the organisation with a role (`admin`, `member` or `viewer`; owner invitations need a signed-in owner). The invitation is valid for 7 days. Counts against the plan's seats. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Headers** | Name | Type | Description | |---|---|---| | `Idempotency-Key` (optional) | string or null | 8-64 characters [A-Za-z0-9_-] | **Request body** ([`CreateInviteRequest`](#schema-createinviterequest)) | Field | Type | Description | |---|---|---| | `email` | string | | | `role` | `owner` \| `admin` \| `member` \| `viewer` | | **Responses** - `201`: [`Invite`](#schema-invite) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `plan_limit` (seats), `org_suspended`, `org_deleting`, `billing_inactive`; `404` `not_found`; `409` `conflict` (already a member or invited), `idempotency_in_progress`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`, `idempotency_mismatch`; `429` `rate_limited` (20 per day) ### Revoke an invitation `DELETE /orgs/{org}/invites/{inv}` · token role: `admin` The link in the invitation email stops working. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `inv` | id `inv_…` | Invitation id (`inv_…`) | **Responses** - `204`: no body (revoked) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `billing_inactive`; `404` `not_found`; `429` `rate_limited` ### Resend an invitation `POST /orgs/{org}/invites/{inv}/resend` · token role: `admin` Send the invitation again with a new link (the old link stops working) and a fresh 7-day expiry. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `inv` | id `inv_…` | Invitation id (`inv_…`) | **Responses** - `202`: no body (sent) - Errors: `401` `unauthenticated`; `403` `forbidden`, `plan_limit`, `org_suspended`, `org_deleting`, `billing_inactive`; `404` `not_found`; `429` `rate_limited` ### List members `GET /orgs/{org}/members` · token role: `viewer` Every member of the organisation with their role and when they were last active. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Responses** - `200`: list of [`Member`](#schema-member) - Errors: `401` `unauthenticated`; `403` `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ### Change a member's role `PATCH /orgs/{org}/members/{usr}` · token role: `admin` Set a member's role to `admin`, `member` or `viewer`. Granting or removing `owner` is not possible with a token. If the member is demoted below the role of one of their API tokens, that token is revoked. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `usr` | id `usr_…` | User id (`usr_…`) | **Request body** ([`RoleRequest`](#schema-rolerequest)) | Field | Type | Description | |---|---|---| | `role` | `owner` \| `admin` \| `member` \| `viewer` | | **Responses** - `200`: [`Member`](#schema-member) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `billing_inactive`; `404` `not_found`; `409` `sole_owner`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### Remove a member `DELETE /orgs/{org}/members/{usr}` · token role: `admin` Remove a member from the organisation; their API tokens for it are deleted. Removing an owner, or the token's own creator (leaving), is not possible with a token. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `usr` | id `usr_…` | User id (`usr_…`) | **Responses** - `204`: no body (removed) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `409` `sole_owner`; `429` `rate_limited` ### Organisation overview `GET /orgs/{org}/overview` · token role: `viewer` Monitor counts by status, open incidents (newest first, at most 20), the latest status changes and usage against the plan. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Responses** - `200`: [`Overview`](#schema-overview) - Errors: `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ## Monitors ### List locations `GET /locations` · token role: none (public) The active check locations and the regions they belong to. Monitors name locations by `code` (`lon1`) or by region. No authentication needed. **Responses** - `200`: [`LocationsResponse`](#schema-locationsresponse) - Errors: `429` `rate_limited` ### List monitors `GET /orgs/{org}/monitors` · token role: `viewer` The organisation's monitors, filtered and sorted, with cursor pagination (up to 500 per page). **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Query parameters** | Name | Type | Description | |---|---|---| | `status` (optional) | string | Comma-separated statuses: `unknown,up,down,degraded,no_data,paused`. | | `kind` (optional) | string | Comma-separated kinds. | | `label` (optional) | string | One label. | | `run_state` (optional) | string | Comma-separated run states: `active,paused,system_paused`. | | `contact_id` (optional) | string | Monitors linked to this contact. | | `q` (optional) | string | Substring of the name or target (≤ 100 characters). | | `sort` (optional) | string | `created_at` (default) \| `name` \| `status` \| `status_since`. | | `dir` (optional) | string | `asc` (default) \| `desc`. | | `limit` (optional) | integer | 1-500, default 50. | | `cursor` (optional) | string | `next_cursor` of the previous page. | **Responses** - `200`: page of [`Monitor`](#schema-monitor) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Create a monitor `POST /orgs/{org}/monitors` · token role: `member` Validate and create a monitor. It starts checking at once unless `paused` is true. Send an `Idempotency-Key` so a retried request does not create a second monitor. `403 plan_limit` names the limit that would be exceeded. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Headers** | Name | Type | Description | |---|---|---| | `Idempotency-Key` (optional) | string or null | 8-64 characters [A-Za-z0-9_-] | **Request body** ([`MonitorInput`](#schema-monitorinput)) | Field | Type | Description | |---|---|---| | `contact_ids` (optional) | array of id `con_…` or null | At most 100; omitted = every `auto_attach` contact. | | `degraded_after_ms` (optional) | integer or null | `http_adv` only; 100-30000 and below the time budget; `null` = off. | | `failure_threshold` (optional) | integer or null | 1-10, default 2; for `domain_expiry` 1, the default (a second probe confirms each change instead); for `heartbeat` 1. | | `interval_s` | integer | 30, 60, 120, 300, 600, 900, 1800 or 3600, at least the plan minimum. `domain_expiry`: 86400 only. `heartbeat`: the period between pings, 60-604800 (no plan minimum). | | `kind` | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` | Immutable after create. | | `labels` (optional) | array of string | At most 10, each 1-32 characters of `[a-z0-9-_.]`. | | `locations` (optional) | [`MonitorLocationsInput`](#schema-monitorlocationsinput) or null | Required, except for `domain_expiry`, which must omit it: each of its lookups runs from one location, a different one each day; and for `heartbeat`, which runs no checks. | | `name` | string | 1-100 characters. | | `paused` (optional) | boolean | | | `recovery_threshold` (optional) | integer or null | 1-10, default 2; for `domain_expiry` and `heartbeat` 1, the default. | | `reminder_interval_s` (optional) | integer or null | `null` = no reminders; 900-86400. | | `spec` | [`MonitorSpecInput`](#schema-monitorspecinput) | The per-kind spec without the type tag: the `MonitorSpecInput` schema of `kind`; secret fields are write-only. Fields per kind: [Monitor kinds](../monitor-kinds.md). | **Responses** - `201`: [`Monitor`](#schema-monitor), header `ETag` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `plan_limit`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `idempotency_in_progress`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`, `idempotency_mismatch`; `429` `rate_limited` ### Run a check once `POST /orgs/{org}/monitors/preview-check` · token role: `member` Run a check from one location now and return the result, without creating a monitor or storing anything. The same validation as creating a monitor applies. Waits for the probe (up to the check's time budget plus 15 seconds); `503 unavailable` if no probe in that location can run it. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Request body** ([`PreviewCheckInput`](#schema-previewcheckinput)) | Field | Type | Description | |---|---|---| | `kind` | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` | | | `location` | string | Location code. | | `spec` | [`MonitorSpecInput`](#schema-monitorspecinput) | The spec as in `MonitorInput` (secrets inline). Fields per kind: [Monitor kinds](../monitor-kinds.md). | **Responses** - `200`: [`CheckResult`](#schema-checkresult) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited`; `503` `unavailable` ### Get a monitor `GET /orgs/{org}/monitors/{mon}` · token role: `viewer` One monitor with its current state per location and its uptime over the last 24 hours. The `ETag` header carries `version` for `If-Match`. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Responses** - `200`: [`Monitor`](#schema-monitor), header `ETag` - Errors: `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ### Update a monitor `PATCH /orgs/{org}/monitors/{mon}` · token role: `member` Change any field except `kind` (JSON merge patch: omitted fields are unchanged, `null` clears a nullable field, arrays are replaced whole; `spec` is merged field by field). Send `If-Match: ""` to get `412 precondition_failed` instead of overwriting someone else's change. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Headers** | Name | Type | Description | |---|---|---| | `If-Match` (optional) | string or null | "" | **Request body** ([`MonitorPatch`](#schema-monitorpatch)) | Field | Type | Description | |---|---|---| | `contact_ids` (optional) | array of id `con_…` or null | | | `degraded_after_ms` (optional) | integer or null | | | `failure_threshold` (optional) | integer or null | | | `interval_s` (optional) | integer or null | | | `kind` (optional) | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` or null | Not allowed (`422 not_allowed`): the kind is immutable. | | `labels` (optional) | array of string or null | | | `locations` (optional) | [`MonitorLocationsInput`](#schema-monitorlocationsinput) or null | | | `name` (optional) | string or null | | | `paused` (optional) | boolean or null | `false` runs the same checks as `POST .../resume`. | | `recovery_threshold` (optional) | integer or null | | | `reminder_interval_s` (optional) | integer or null | | | `spec` (optional) | object or null | Merged into the stored spec (RFC 7396): any subset of the `MonitorSpecInput` schema of the monitor's kind. Fields per kind: [Monitor kinds](../monitor-kinds.md). | **Responses** - `200`: [`Monitor`](#schema-monitor), header `ETag` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `plan_limit`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `412` `precondition_failed`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### Delete a monitor `DELETE /orgs/{org}/monitors/{mon}` · token role: `member` Delete a monitor with its results, incidents and pending notifications. No notification is sent. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Headers** | Name | Type | Description | |---|---|---| | `If-Match` (optional) | string or null | "" | **Responses** - `204`: no body (deleted) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `412` `precondition_failed`; `422` `validation`; `429` `rate_limited` ### List a monitor's contacts `GET /orgs/{org}/monitors/{mon}/contacts` · token role: `viewer` The contacts notified for a monitor. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Responses** - `200`: list of [`Contact`](#schema-contact) - Errors: `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ### Set a monitor's contacts `PUT /orgs/{org}/monitors/{mon}/contacts` · token role: `member` Replace the set of contacts notified for a monitor. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Headers** | Name | Type | Description | |---|---|---| | `If-Match` (optional) | string or null | "" of the monitor | **Request body** ([`MonitorContactIds`](#schema-monitorcontactids)) | Field | Type | Description | |---|---|---| | `contact_ids` | array of id `con_…` | At most 100 contacts of the org; replaces the set. | **Responses** - `200`: [`MonitorContactIds`](#schema-monitorcontactids) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `412` `precondition_failed`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### List a monitor's incidents `GET /orgs/{org}/monitors/{mon}/incidents` · token role: `viewer` Incidents of one monitor, newest first, within the plan's history. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `limit` (optional) | integer | 1-200, default 50. | | `cursor` (optional) | string | `next_cursor` of the previous page. | **Responses** - `200`: page of [`Incident`](#schema-incident) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Pause a monitor `POST /orgs/{org}/monitors/{mon}/pause` · token role: `member` Stop checking until resumed. Pausing resolves an open incident. Idempotent. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Responses** - `200`: [`Monitor`](#schema-monitor), header `ETag` - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `429` `rate_limited` ### List heartbeat pings `GET /orgs/{org}/monitors/{mon}/pings` · token role: `viewer` A heartbeat monitor's ping history, newest first: when each ping arrived, `success` or `fail` (the job requested the ping URL followed by `/fail`), the method, the client's address and user agent. The newest 100 are kept; other kinds have none. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `limit` (optional) | integer | 1-100, default 50. | **Responses** - `200`: list of `HeartbeatPing` - Errors: `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### List check results `GET /orgs/{org}/monitors/{mon}/results` · token role: `viewer` Raw results of a monitor, newest first. The window defaults to the last 24 hours, spans at most 7 days, and starts no earlier than the plan's raw data retention. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `from` (optional) | string | Default `to − 24 h`; at most 7 days before `to`; clamped to the plan's raw retention. | | `to` (optional) | string | Default now. | | `location` (optional) | string | Location code. | | `outcome` (optional) | string | Comma-separated outcomes. | | `limit` (optional) | integer | 1-1000, default 50. | | `cursor` (optional) | string | `next_cursor` of the previous page. | **Responses** - `200`: page of [`CheckResult`](#schema-checkresult) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Resume a monitor `POST /orgs/{org}/monitors/{mon}/resume` · token role: `member` Start checking again. `403 plan_limit` if the plan no longer allows the monitor. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Responses** - `200`: [`Monitor`](#schema-monitor), header `ETag` - Errors: `401` `unauthenticated`; `403` `forbidden`, `plan_limit`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### List rollups `GET /orgs/{org}/monitors/{mon}/rollups` · token role: `viewer` Hourly (`hour`, at most 31 days) or daily (`day`, at most 731 days) aggregates per location: check counts by outcome and latency statistics. Not paginated. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `granularity` (optional) | string | `hour` (default) or `day`. | | `from` (optional) | string | Default `to − 24 h` (hour) or `to − 30 days` (day); at most 31 / 731 days before `to`; clamped to plan history. | | `to` (optional) | string | | | `location` (optional) | string | Location code. | **Responses** - `200`: [`RollupsResponse`](#schema-rollupsresponse) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Response-time series `GET /orgs/{org}/monitors/{mon}/series` · token role: `viewer` Median response time per location over `24h`, `7d`, `30d` or `90d`, with the periods the monitor was not up. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `range` (optional) | string | `24h` (default) \| `7d` \| `30d` \| `90d`. | **Responses** - `200`: [`Series`](#schema-series) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Status timeline `GET /orgs/{org}/monitors/{mon}/timeline` · token role: `viewer` The status periods (up, down, degraded, no data, paused) overlapping a window, oldest first. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `from` (optional) | string | Default `to − 24 h`; clamped to plan history. | | `to` (optional) | string | Default now. | **Responses** - `200`: list of `StatusPeriod` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### List path traces `GET /orgs/{org}/monitors/{mon}/traces` · token role: `viewer` Traceroute/MTR runs towards a monitor's target, newest first, within the plan's raw data retention. Traces run automatically when an incident opens. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `incident_id` (optional) | string | Only the traces run for this incident. | | `limit` (optional) | integer | 1-50, default 10. | **Responses** - `200`: list of [`PathTrace`](#schema-pathtrace) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Run a path trace `POST /orgs/{org}/monitors/{mon}/traces` · token role: `member` Trace the network path to the monitor's target from a location (`mtr` by default, about 15 seconds; `traceroute` about 5). Returns at once with `status: pending`; poll `GET /orgs/{org}/traces/{trc}` for the result. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Request body** ([`PathTraceInput`](#schema-pathtraceinput)) | Field | Type | Description | |---|---|---| | `location` | string | Location code to trace from (any active location). | | `mode` (optional) | `traceroute` \| `mtr` or null | Default `mtr`. | | `protocol` (optional) | `icmp` \| `tcp` or null | Default: `tcp` to the target's port when it has one (falling back to `icmp` when no probe there can trace over TCP), else `icmp`. | **Responses** - `202`: [`PathTrace`](#schema-pathtrace) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `conflict` (a trace from this location is still running); `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited`; `503` `unavailable` (no probe there can run it) ### Uptime `GET /orgs/{org}/monitors/{mon}/uptime` · token role: `viewer` Uptime ratio and seconds down, degraded, without data and paused, for named windows (`24h`, `7d`, `30d`, `90d`) or a `from`/`to` range. Degraded counts as up; paused and no-data time is left out of the ratio. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mon` | id `mon_…` | Monitor id (`mon_…`) | **Query parameters** | Name | Type | Description | |---|---|---| | `from` (optional) | string | | | `to` (optional) | string | | | `windows` (optional) | string | Comma-separated `24h,7d,30d,90d` (default all four). | **Responses** - `200`: list of `Uptime` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Get a path trace `GET /orgs/{org}/traces/{trc}` · token role: `viewer` One path trace with its hops. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `trc` | id `trc_…` | Path trace id (`trc_…`) | **Responses** - `200`: [`PathTrace`](#schema-pathtrace) - Errors: `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ## Incidents ### List incidents `GET /orgs/{org}/incidents` · token role: `viewer` The organisation's incidents, newest first, filtered by status, monitor, severity and time. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Query parameters** | Name | Type | Description | |---|---|---| | `status` (optional) | string | `open` \| `resolved`. | | `monitor_id` (optional) | string | | | `severity` (optional) | string | `down` \| `degraded`. | | `from` (optional) | string | Opened at or after (clamped to plan history; open incidents are always listed). | | `to` (optional) | string | Opened before. | | `limit` (optional) | integer | 1-200, default 50. | | `cursor` (optional) | string | `next_cursor` of the previous page. | **Responses** - `200`: page of [`Incident`](#schema-incident) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Get an incident `GET /orgs/{org}/incidents/{inc}` · token role: `viewer` One incident with its timeline events and the notifications it sent. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `inc` | id `inc_…` | Incident id (`inc_…`) | **Responses** - `200`: [`IncidentDetail`](#schema-incidentdetail) - Errors: `401` `unauthenticated`; `403` `forbidden`, `account_suspended`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ### Acknowledge an incident `POST /orgs/{org}/incidents/{inc}/acknowledge` · token role: `member` Mark an open incident as acknowledged: reminders stop. Idempotent; `409 incident_resolved` once it is resolved. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `inc` | id `inc_…` | Incident id (`inc_…`) | **Responses** - `200`: [`Incident`](#schema-incident) - Errors: `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `incident_resolved`; `429` `rate_limited`; `503` `unavailable` ### Add a note to an incident `POST /orgs/{org}/incidents/{inc}/notes` · token role: `member` Add a note (1-2000 characters) to an incident's timeline. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `inc` | id `inc_…` | Incident id (`inc_…`) | **Request body** ([`IncidentNoteInput`](#schema-incidentnoteinput)) | Field | Type | Description | |---|---|---| | `body` | string | 1-2000 characters. | **Responses** - `201`: [`IncidentEvent`](#schema-incidentevent) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ## Contacts ### List contacts `GET /orgs/{org}/contacts` · token role: `viewer` The organisation's contacts (email, SMS, phone calls, Pushover, Discord, PagerDuty, Slack, webhooks), oldest first. Secrets are never returned. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Query parameters** | Name | Type | Description | |---|---|---| | `kind` (optional) | string | `email \| sms \| voice \| pushover \| discord \| pagerduty \| slack \| webhook \| teams`. | | `status` (optional) | string | `unverified \| active \| disabled`. | | `limit` (optional) | integer | 1-200, default 50. | | `cursor` (optional) | string | `next_cursor` of the previous page. | **Responses** - `200`: page of [`Contact`](#schema-contact) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `422` `validation`; `429` `rate_limited` ### Create a contact `POST /orgs/{org}/contacts` · token role: `member` Create a contact. Email, SMS and voice contacts start `unverified` and are sent a 6-digit code (an SMS code uses one SMS credit; a voice contact gets a call that reads the code, which uses a call's credits); Pushover and Discord contacts are validated with the provider and start `active`; PagerDuty contacts (an Events API v2 integration key) are checked offline and start `active`; Slack contacts (an incoming webhook URL) are checked with Slack and start `active`; webhook contacts start `active` when their host has the organisation's TXT record (`config.txt_name`, `config.txt_value`), else `unverified` until a re-check finds it; Microsoft Teams contacts (a Workflows webhook URL) start `unverified` and get a 6-digit code posted into the channel. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Headers** | Name | Type | Description | |---|---|---| | `Idempotency-Key` (optional) | string or null | 8-64 characters [A-Za-z0-9_-] | **Request body** ([`ContactInput`](#schema-contactinput)) | Field | Type | Description | |---|---|---| | `auto_attach` (optional) | boolean or null | Attach to monitors created without `contact_ids`. Default `false`. | | `config` (optional) | object | Per kind: email `{"address"}`, sms and voice `{"phone"}` (E.164; a voice contact may be a landline), pushover `{"device", "priority_down", "priority_up", "sound"}`, discord `{}`, pagerduty `{"region"}` (`us` or `eu`, default `us`), slack `{}`, webhook `{}`, teams `{}`. | | `kind` | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` | | | `name` (optional) | string | 1-100 characters. | | `notify_on` (optional) | array of `down` \| `recovered` \| `degraded` or null | Non-empty subset of `down`, `recovered`, `degraded`; default `["down", "recovered"]`. | | `secret` (optional) | object or null | Write-only: pushover `{"user_key"}`, discord `{"webhook_url"}`, pagerduty `{"routing_key"}` (an Events API v2 integration key), slack `{"webhook_url"}` (an incoming webhook), webhook `{"url"}` (`https://` on a hostname whose `_justmonitorit.` TXT record is the org's `txt_value`), teams `{"webhook_url"}` (a Workflows webhook on `*.logic.azure.com` or `*.environment.api.powerplatform.com`); none for email, sms and voice. | **Responses** - `201`: [`Contact`](#schema-contact), header `ETag` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `plan_limit`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `sms_allowance_exhausted`, `idempotency_in_progress`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`, `idempotency_mismatch`; `429` `rate_limited`; `503` `unavailable` ### Get a contact `GET /orgs/{org}/contacts/{con}` · token role: `viewer` One contact. The `ETag` header carries `version` for `If-Match`. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Responses** - `200`: [`Contact`](#schema-contact), header `ETag` - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `429` `rate_limited` ### Update a contact `PATCH /orgs/{org}/contacts/{con}` · token role: `member` Change any field except `kind` (JSON merge patch). Changing an email address, phone number or Teams webhook URL makes the contact `unverified` again and sends a new code. A `secret` that is omitted is kept. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Headers** | Name | Type | Description | |---|---|---| | `If-Match` (optional) | string or null | "" | **Request body** ([`ContactPatch`](#schema-contactpatch)) | Field | Type | Description | |---|---|---| | `auto_attach` (optional) | boolean or null | | | `config` (optional) | object or null | | | `kind` (optional) | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` or null | Not allowed (`422 not_allowed`). | | `name` (optional) | string or null | | | `notify_on` (optional) | array of `down` \| `recovered` \| `degraded` or null | | | `secret` (optional) | object or null | | **Responses** - `200`: [`Contact`](#schema-contact), header `ETag` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `conflict`, `sms_allowance_exhausted`; `412` `precondition_failed`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited`; `503` `unavailable` ### Delete a contact `DELETE /orgs/{org}/contacts/{con}` · token role: `member` Delete a contact; it is removed from every monitor and its pending notifications are dropped. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Headers** | Name | Type | Description | |---|---|---| | `If-Match` (optional) | string or null | "" | **Responses** - `204`: no body (deleted) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `412` `precondition_failed`; `422` `validation`; `429` `rate_limited` ### Disable a contact `POST /orgs/{org}/contacts/{con}/disable` · token role: `member` Stop sending to a contact (`disabled_reason = "user"`) without deleting it. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Responses** - `200`: [`Contact`](#schema-contact), header `ETag` - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `429` `rate_limited` ### Enable a contact `POST /orgs/{org}/contacts/{con}/enable` · token role: `member` Re-enable a contact disabled by a user or after repeated delivery failures. Other disabled states (bounce, complaint, unsubscribe, invalid) return through verification or a signed-in owner or admin. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Responses** - `200`: [`Contact`](#schema-contact), header `ETag` - Errors: `401` `unauthenticated`; `403` `forbidden` (also: a bounced address and the caller is not an owner or admin), `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `conflict` (also: the address bounced before it was verified, or the email provider lists it for a complaint); `429` `rate_limited` (also: a bounced address was reactivated less than 24 h ago); `503` `unavailable` (the email provider could not reactivate the address) ### Set a contact's monitors `PUT /orgs/{org}/contacts/{con}/monitors` · token role: `member` Replace the set of monitors a contact is notified for. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Headers** | Name | Type | Description | |---|---|---| | `If-Match` (optional) | string or null | "" of the contact | **Request body** ([`ContactMonitors`](#schema-contactmonitors)) | Field | Type | Description | |---|---|---| | `monitor_ids` | array of id `mon_…` | At most 1,000; replaces the contact's monitor links. | **Responses** - `200`: [`ContactMonitors`](#schema-contactmonitors), header `ETag` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `412` `precondition_failed`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### Send a test notification `POST /orgs/{org}/contacts/{con}/test` · token role: `member` Queue a test message to an active contact and return the notification id; follow it with `GET /orgs/{org}/notifications/{ntf}`. `409 contact_unverified`, `contact_disabled` or `sms_allowance_exhausted` when it cannot be sent. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Headers** | Name | Type | Description | |---|---|---| | `Idempotency-Key` (optional) | string or null | 8-64 characters [A-Za-z0-9_-] | **Responses** - `202`: [`TestSent`](#schema-testsent) - Errors: `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `contact_unverified`, `contact_disabled`, `sms_allowance_exhausted`, `idempotency_in_progress`, `conflict`; `422` `validation`, `idempotency_mismatch`; `429` `rate_limited` ### Verify a contact `POST /orgs/{org}/contacts/{con}/verify` · token role: `member` Verify an email, SMS, voice or Teams contact with the 6-digit code it received. A wrong code is `422 validation` with `errors[0].code = "invalid_code"`; after 5 wrong codes the code is void. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Request body** ([`VerifyInput`](#schema-verifyinput)) | Field | Type | Description | |---|---|---| | `code` | string | The 6-digit code. | **Responses** - `200`: [`Contact`](#schema-contact), header `ETag` - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `conflict`; `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation` (invalid_code, with attempts_left); `429` `rate_limited` ### Send a verification code `POST /orgs/{org}/contacts/{con}/verify/send` · token role: `member` Send a new code to an email, SMS, voice or Teams contact, or validate a Pushover, Discord, PagerDuty or Slack contact again, or look for a webhook's TXT record again (it becomes `active` on success). **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `con` | id `con_…` | Contact id (`con_…`) | **Responses** - `202`: no body (code sent, or validation passed) - Errors: `401` `unauthenticated`; `403` `forbidden`, `billing_inactive`, `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `409` `conflict`, `sms_allowance_exhausted`; `422` `validation` (pushover_invalid, `discord_webhook_invalid)`; `429` `rate_limited`; `503` `unavailable` ## Notifications ### List notifications `GET /orgs/{org}/notifications` · token role: `viewer` The organisation's notification log, newest first, with each delivery attempt. Filter by contact, incident, monitor, status or kind. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Query parameters** | Name | Type | Description | |---|---|---| | `contact_id` (optional) | string | `con_…`: the delivery log of one contact. | | `incident_id` (optional) | string | `inc_…` | | `monitor_id` (optional) | string | `mon_…` | | `status` (optional) | string | `pending \| sending \| sent \| failed \| cancelled \| suppressed` | | `kind` (optional) | string | A notification kind. | | `cursor` (optional) | string | Opaque cursor from `next_cursor`. | | `limit` (optional) | integer | 1-200, default 50. | **Responses** - `200`: page of [`Notification`](#schema-notification) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found` (not a member); `422` `validation` (bad filter or cursor); `429` `rate_limited` ### Get a notification `GET /orgs/{org}/notifications/{ntf}` · token role: `viewer` One notification with its delivery attempts and provider status. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `ntf` | id `ntf_…` | Notification id (`ntf_…`) | **Responses** - `200`: [`Notification`](#schema-notification) - Errors: `401` `unauthenticated`; `403` `org_suspended`, `org_deleting`, `account_suspended`; `404` `not_found`; `429` `rate_limited` ## Maintenance windows ### List maintenance windows `GET /orgs/{org}/maintenance` · token role: `viewer` Active and upcoming maintenance windows, and those that ended in the last 30 days, by start. While a window is active, the monitors it covers keep checking and record incidents, but their alerts are held until it ends. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Responses** - `200`: list of [`MaintenanceWindow`](#schema-maintenancewindow) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ### Schedule a maintenance window `POST /orgs/{org}/maintenance` · token role: `member` Hold the alerts of all monitors (`all_monitors: true`, including monitors added later) or of `monitor_ids` from `starts_at` (default now) until `ends_at`, or for `duration_minutes`; at most 7 days. When it ends, each incident still open alerts its contacts once; incidents that ended inside the window send nothing. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | **Request body** ([`CreateMaintenanceRequest`](#schema-createmaintenancerequest)) | Field | Type | Description | |---|---|---| | `all_monitors` (optional) | boolean | Cover every monitor, including ones added later. | | `duration_minutes` (optional) | integer or null | 1-10080, instead of `ends_at`. | | `ends_at` (optional) | timestamp or null | After `starts_at`, at most 7 days later. | | `monitor_ids` (optional) | array of string | The monitors to cover when `all_monitors` is false (at least one). | | `name` | string | 1-100 characters, e.g. "Postgres upgrade". | | `starts_at` (optional) | timestamp or null | Absent = now. At most a year ahead. | **Responses** - `201`: [`MaintenanceWindow`](#schema-maintenancewindow) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `409` `conflict` (50 windows that have not ended); `413` `payload_too_large`; `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### Get a maintenance window `GET /orgs/{org}/maintenance/{mnt}` · token role: `viewer` One maintenance window, with `state` (`upcoming`, `active` or `ended`) and the number of incidents whose alerts it holds now. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mnt` | string | Window id (`mnt_…`) | **Responses** - `200`: [`MaintenanceWindow`](#schema-maintenancewindow) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `429` `rate_limited` ### Change a maintenance window `PATCH /orgs/{org}/maintenance/{mnt}` · token role: `member` Rename a window that has not ended or move its end (for example to extend it); the start can only move before the window starts. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mnt` | string | Window id (`mnt_…`) | **Request body** ([`UpdateMaintenanceRequest`](#schema-updatemaintenancerequest)) | Field | Type | Description | |---|---|---| | `ends_at` (optional) | timestamp or null | | | `name` (optional) | string or null | | | `starts_at` (optional) | timestamp or null | | **Responses** - `200`: [`MaintenanceWindow`](#schema-maintenancewindow) - Errors: `400` `bad_request`; `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `409` `conflict` (the window has ended); `415` `unsupported_media_type`; `422` `validation`; `429` `rate_limited` ### Delete a maintenance window `DELETE /orgs/{org}/maintenance/{mnt}` · token role: `member` Delete a window that has not started. To stop an active window, end it instead. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mnt` | string | Window id (`mnt_…`) | **Responses** - `204`: no body (deleted) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `409` `conflict` (started: end it instead); `429` `rate_limited` ### End a maintenance window now `POST /orgs/{org}/maintenance/{mnt}/end` · token role: `member` End an active window now. Within seconds, each incident still open alerts its contacts once. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | id `org_…` | Organisation id (`org_…`); `GET /token` returns it | | `mnt` | string | Window id (`mnt_…`) | **Responses** - `200`: [`MaintenanceWindow`](#schema-maintenancewindow) - Errors: `401` `unauthenticated`; `403` `forbidden`, `org_suspended`, `org_deleting`; `404` `not_found`; `409` `conflict` (not started: delete it; or already ended); `429` `rate_limited` ## Billing ### List plans `GET /billing/plans` · token role: none (public) The plans on sale with their prices and limits. No authentication needed. **Responses** - `200`: list of `PlanOffer` - Errors: `404` `billing_disabled` (`[billing] mode = "disabled"`); `429` `rate_limited`; `503` `unavailable:` Stripe not configured, unreachable, or a catalogue price is missing ### Billing state `GET /orgs/{org}/billing` · token role: `viewer` The organisation's plan, subscription state, entitlements and usage. Checkout and the customer portal need a signed-in owner. **Path parameters** | Name | Type | Description | |---|---|---| | `org` | string | Organisation id (`org_…`); `GET /token` returns it | **Responses** - `200`: [`Billing`](#schema-billing) - Errors: `401` `unauthenticated`; `403` `account_suspended`, `org_deleting`; `404` `not_found` (unknown org or not a member), `billing_disabled` (`[billing] mode = "disabled"`; use `GET /orgs/{org}/entitlements`); `429` `rate_limited` ## Schemas {#schemas} Response and request objects referenced above. Every field of a response is present; absent values are `null`. ### ApiToken {#schema-apitoken} An API token (never its secret). | Field | Type | Description | |---|---|---| | `created_at` | timestamp | | | `created_by` | [`UserRef`](#schema-userref) | The member the token acts for. | | `expires_at` | timestamp or null | `null` = never expires. | | `id` | id `tok_…` | | | `last_used_at` | timestamp or null | | | `last_used_ip` | string or null | | | `name` | string | | | `prefix` | string | The first 8 characters after `jmi_`, for recognising the token. | | `role` | `admin` \| `member` \| `viewer` | | ### Assertion {#schema-assertion} An `http_adv` assertion. With `type` = `body_contains`: | Field | Type | Description | |---|---|---| | `type` | `body_contains` | | | `value` | string | | With `type` = `body_not_contains`: | Field | Type | Description | |---|---|---| | `type` | `body_not_contains` | | | `value` | string | | With `type` = `body_regex`: | Field | Type | Description | |---|---|---| | `pattern` | string | | | `type` | `body_regex` | | With `type` = `json_path`: | Field | Type | Description | |---|---|---| | `op` | `exists` \| `not_exists` \| `equals` \| `not_equals` \| `contains` \| `lt` \| `gt` | | | `path` | string | | | `type` | `json_path` | | | `value` | any | Any JSON value; required except for `exists` and `not_exists`. | With `type` = `header`: | Field | Type | Description | |---|---|---| | `name` | string | | | `op` | `exists` \| `not_exists` \| `equals` \| `contains` \| `regex` | | | `type` | `header` | | | `value` | string or null | | ### Billing {#schema-billing} An org's subscription, entitlements and usage. | Field | Type | Description | |---|---|---| | `billing_interval` | `month` \| `year` or null | | | `cancel_at_period_end` | boolean | | | `current_period_end` | timestamp or null | | | `entitlements` | [`Entitlements`](#schema-entitlements) | | | `grace_ends_at` | timestamp or null | | | `past_due_since` | timestamp or null | | | `plan` | `trial` \| `starter` \| `pro` \| `business` | | | `status` | `trialing` \| `active` \| `past_due` \| `canceled` | Effective status: a lapsed trial or grace period is `canceled`. | | `trial_ends_at` | timestamp or null | | | `usage` | [`BillingUsage`](#schema-billingusage) | | ### BillingUsage {#schema-billingusage} Current usage of an org (`Billing.usage`). | Field | Type | Description | |---|---|---| | `checks_per_minute` | integer | | | `contacts` | integer | | | `monitors` | integer | | | `seats` | integer | | | `sms_credits_limit` | integer | | | `sms_credits_used` | integer | | | `sms_period_end` | timestamp | Start of the next UTC month, when the credits reset. | | `sms_state` | `ok` \| `warning` \| `exhausted` | | ### CheckResult {#schema-checkresult} One check result. | Field | Type | Description | |---|---|---| | `error_detail` | string or null | | | `error_kind` | `dns_failure` \| `blocked_address` \| `connect_timeout` \| `connection_refused` \| `connection_reset` \| `host_unreachable` \| `tls_error` \| `cert_expired` \| `timeout` \| `protocol_error` \| `status_mismatch` \| `assertion_failed` \| `too_many_redirects` \| `redirect_blocked` \| `packet_loss` \| `slow_response` \| `cert_expiring` \| `domain_expiring` \| `domain_expired` \| `domain_not_found` \| `rdap_unavailable` \| `heartbeat_missed` \| `heartbeat_failed` \| `network_unreachable` \| `icmp_unavailable` \| `resource_exhausted` \| `probe_overloaded` \| `internal` or null | | | `latency_ms` | integer or null | | | `location` | string | Code of the location that ran the check. | | `metrics` | object or null | | | `outcome` | `up` \| `degraded` \| `down` \| `error` | | | `resolved_ip` | string or null | | | `slot_location` | string or null | Code of the configured location it stood in for, if substituted. | | `status_code` | integer or null | | | `timings` | [`CheckTimings`](#schema-checktimings) | | | `tls_not_after` | timestamp or null | | | `ts` | timestamp | | | `work_kind` | `round` \| `recheck` \| `shadow` \| `canary` \| `preview` \| `trace` | | ### CheckTimings {#schema-checktimings} Phase timings of a result. | Field | Type | Description | |---|---|---| | `connect_ms` | integer or null | | | `dns_ms` | integer or null | | | `tls_ms` | integer or null | | | `total_ms` | integer or null | | | `ttfb_ms` | integer or null | | ### Contact {#schema-contact} A contact (secrets are never returned; `secret_set` says whether one is stored). | Field | Type | Description | |---|---|---| | `auto_attach` | boolean | | | `config` | object | Stored config: email `{"address"}`, sms and voice `{"phone_e164", "country_code"}`, pushover `{"device", "priority_down", "priority_up", "sound", "devices"}`, discord `{"webhook_id", "webhook_name", "channel_id", "guild_id"}`, pagerduty `{"region", "key_hint"}`, slack `{"team_id", "webhook_id"}`, webhook `{"host", "txt_name", "txt_value"}` (the TXT record that verifies the host), teams `{"host", "workflow_id"}`. | | `consecutive_failures` | integer | | | `created_at` | timestamp | | | `credits_per_call` | integer or null | Voice only: the SMS credits each call to this number uses, from what the number costs to call. `null` for other kinds. | | `disabled_reason` | `user` \| `bounced` \| `complaint` \| `opted_out` \| `invalid` \| `unsubscribed` \| `failing` \| `abuse` or null | | | `id` | id `con_…` | | | `kind` | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` | | | `last_delivery_at` | timestamp or null | | | `last_error` | string or null | | | `monitor_count` | integer | | | `name` | string | | | `notify_on` | array of `down` \| `recovered` \| `degraded` | | | `secret_set` | boolean | | | `status` | `unverified` \| `active` \| `disabled` | | | `updated_at` | timestamp | | | `verified_at` | timestamp or null | | | `version` | integer | Optimistic-concurrency version (also the `ETag`). | ### ContactInput {#schema-contactinput} `POST /orgs/{org}/contacts` body. | Field | Type | Description | |---|---|---| | `auto_attach` (optional) | boolean or null | Attach to monitors created without `contact_ids`. Default `false`. | | `config` (optional) | object | Per kind: email `{"address"}`, sms and voice `{"phone"}` (E.164; a voice contact may be a landline), pushover `{"device", "priority_down", "priority_up", "sound"}`, discord `{}`, pagerduty `{"region"}` (`us` or `eu`, default `us`), slack `{}`, webhook `{}`, teams `{}`. | | `kind` | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` | | | `name` (optional) | string | 1-100 characters. | | `notify_on` (optional) | array of `down` \| `recovered` \| `degraded` or null | Non-empty subset of `down`, `recovered`, `degraded`; default `["down", "recovered"]`. | | `secret` (optional) | object or null | Write-only: pushover `{"user_key"}`, discord `{"webhook_url"}`, pagerduty `{"routing_key"}` (an Events API v2 integration key), slack `{"webhook_url"}` (an incoming webhook), webhook `{"url"}` (`https://` on a hostname whose `_justmonitorit.` TXT record is the org's `txt_value`), teams `{"webhook_url"}` (a Workflows webhook on `*.logic.azure.com` or `*.environment.api.powerplatform.com`); none for email, sms and voice. | ### ContactMonitors {#schema-contactmonitors} `PUT .../monitors` body and response. | Field | Type | Description | |---|---|---| | `monitor_ids` | array of id `mon_…` | At most 1,000; replaces the contact's monitor links. | ### ContactPatch {#schema-contactpatch} `PATCH /orgs/{org}/contacts/{con}` body (JSON merge patch; `kind` is immutable, `config` is merged, `secret` replaces the stored one). | Field | Type | Description | |---|---|---| | `auto_attach` (optional) | boolean or null | | | `config` (optional) | object or null | | | `kind` (optional) | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` or null | Not allowed (`422 not_allowed`). | | `name` (optional) | string or null | | | `notify_on` (optional) | array of `down` \| `recovered` \| `degraded` or null | | | `secret` (optional) | object or null | | ### ContactRef {#schema-contactref} `{ "id": "con_…", "name": "…", "kind": "sms" }`. | Field | Type | Description | |---|---|---| | `id` | id `con_…` | | | `kind` | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` | | | `name` | string | | ### CreateInviteRequest {#schema-createinviterequest} `{email, role}`. | Field | Type | Description | |---|---|---| | `email` | string | | | `role` | `owner` \| `admin` \| `member` \| `viewer` | | ### CreateMaintenanceRequest {#schema-createmaintenancerequest} `POST /orgs/{org}/maintenance` body. Give `ends_at` or `duration_minutes`, and `all_monitors: true` or `monitor_ids`. | Field | Type | Description | |---|---|---| | `all_monitors` (optional) | boolean | Cover every monitor, including ones added later. | | `duration_minutes` (optional) | integer or null | 1-10080, instead of `ends_at`. | | `ends_at` (optional) | timestamp or null | After `starts_at`, at most 7 days later. | | `monitor_ids` (optional) | array of string | The monitors to cover when `all_monitors` is false (at least one). | | `name` | string | 1-100 characters, e.g. "Postgres upgrade". | | `starts_at` (optional) | timestamp or null | Absent = now. At most a year ahead. | ### DeliveryView {#schema-deliveryview} One delivery attempt of a notification. | Field | Type | Description | |---|---|---| | `attempt` | integer | | | `error` | string or null | | | `error_code` | string or null | | | `http_status` | integer or null | | | `id` | id `dlv_…` | | | `latency_ms` | integer | | | `provider_status` | `delivered` \| `undelivered` \| `failed` \| `bounced` \| `complaint` \| `acknowledged` or null | | | `provider_status_at` | timestamp or null | | | `started_at` | timestamp | | | `status` | `sent` \| `failed_retryable` \| `failed_permanent` \| `ambiguous` | | ### DomainExpirySpec {#schema-domainexpiryspec} `domain_expiry`. | Field | Type | Description | |---|---|---| | `domain` | string | The registered domain, IDNA A-label, lower case. | | `expiry_warn_days` | integer | Degraded this many days before expiry, 1-365 (default 30). | | `timeout_ms` | integer | 1000-30000 (default 10000). | ### Entitlements {#schema-entitlements} The single answer to "what may this org do now". | Field | Type | Description | |---|---|---| | `history_retention_days` | integer | | | `max_checks_per_minute` | integer | | | `max_contacts` | integer | | | `max_locations_per_monitor` | integer | | | `max_monitors` | integer | | | `max_seats` | integer | | | `min_interval_s` | integer | | | `min_locations_per_monitor` | integer | | | `monitors_may_run` | boolean | | | `plan` | `trial` \| `starter` \| `pro` \| `business` | | | `raw_retention_days` | integer | | | `sms_credits_per_month` | integer | | | `status` | `trialing` \| `active` \| `past_due` \| `canceled` | Effective: a lapsed trial or grace period counts as `canceled`. | | `writes_allowed` | boolean | | ### HeaderSpec {#schema-headerspec} One request header of an `http_adv` check. | Field | Type | Description | |---|---|---| | `name` | string | | | `secret` | boolean | | | `value` | string or null | The header value. With `secret`, write-only: sent in requests, `null` in responses (the value lives in the secrets blob). | ### HeartbeatSpec {#schema-heartbeatspec} `heartbeat`. The period between pings is the monitor's `interval_s`. | Field | Type | Description | |---|---|---| | `grace_s` | integer | How late a ping may be before the monitor is down, 60-604800 s (default 300). | ### HttpAdvSpec {#schema-httpadvspec} `http_adv` spec in responses. A secret header has `"secret": true, "value": null`. | Field | Type | Description | |---|---|---| | `assertions` | array of [`Assertion`](#schema-assertion) | | | `auth` | [`HttpAuthView`](#schema-httpauthview) | | | `body` | string or null | | | `body_max_bytes` | integer | | | `cert_expiry_warn_days` | integer or null | | | `expected_status` | array of string | | | `follow_redirects` | boolean | | | `headers` | array of [`HeaderSpec`](#schema-headerspec) | | | `ip_family` | `auto` \| `ipv4` \| `ipv6` | | | `max_redirects` | integer | | | `method` | `GET` \| `HEAD` \| `POST` \| `PUT` \| `PATCH` \| `DELETE` \| `OPTIONS` | | | `timeout_ms` | integer | | | `url` | string | | | `verify_tls` | boolean | | ### HttpAdvSpecInput {#schema-httpadvspecinput} `http_adv` spec in requests. A header with `secret: true` carries its write-only value in `value`. | Field | Type | Description | |---|---|---| | `assertions` (optional) | array of [`Assertion`](#schema-assertion) | At most 20. | | `auth` (optional) | [`HttpAuthInput`](#schema-httpauthinput) or null | Default `{"type": "none"}`. | | `body` (optional) | string or null | Not with `GET` or `HEAD`; at most 16 KiB. | | `body_max_bytes` (optional) | integer | Default 65536. | | `cert_expiry_warn_days` (optional) | integer or null | Default 14; `null` disables the warning. | | `expected_status` (optional) | array of string | Codes (`"200"`) or ranges (`"200-299"`); default `["200-299"]`. | | `follow_redirects` (optional) | boolean | Default `true`. | | `headers` (optional) | array of [`HeaderSpec`](#schema-headerspec) | At most 32; a secret header needs a unique name. | | `ip_family` (optional) | `auto` \| `ipv4` \| `ipv6` | | | `max_redirects` (optional) | integer | 0-10, default 5. | | `method` (optional) | `GET` \| `HEAD` \| `POST` \| `PUT` \| `PATCH` \| `DELETE` \| `OPTIONS` | Default `GET`. | | `timeout_ms` (optional) | integer | Default 10000. | | `url` | string | | | `verify_tls` (optional) | boolean | Default `true`. | ### HttpAuthInput {#schema-httpauthinput} `auth` of an `http_adv` spec in requests. A secret that is absent or `null` keeps the stored value; `""` clears it. With `type` = `none`: | Field | Type | Description | |---|---|---| | `type` | `none` | | With `type` = `basic`: | Field | Type | Description | |---|---|---| | `password` (optional) | string or null | Write-only. | | `type` | `basic` | | | `username` | string | | With `type` = `bearer`: | Field | Type | Description | |---|---|---| | `token` (optional) | string or null | Write-only. | | `type` | `bearer` | | ### HttpAuthView {#schema-httpauthview} `auth` of an `http_adv` spec in responses: whether each secret is stored, never its value. With `type` = `none`: | Field | Type | Description | |---|---|---| | `type` | `none` | | With `type` = `basic`: | Field | Type | Description | |---|---|---| | `password_set` | boolean | | | `type` | `basic` | | | `username` | string | | With `type` = `bearer`: | Field | Type | Description | |---|---|---| | `token_set` | boolean | | | `type` | `bearer` | | ### HttpSpec {#schema-httpspec} `http`. | Field | Type | Description | |---|---|---| | `ip_family` | `auto` \| `ipv4` \| `ipv6` | | | `keyword` | string or null | Text the body must contain, 1-128 bytes. | | `timeout_ms` | integer | 1000-30000 (default 10000). | | `url` | string | `http` or `https` URL (normalised, fragment dropped). | ### IcmpSpec {#schema-icmpspec} `icmp`. | Field | Type | Description | |---|---|---| | `count` | integer | Pings per check, 1-10 (default 3). | | `host` | string | Host name or public IP address. | | `interval_ms` | integer | Between pings, 200-1000 (default 500). | | `ip_family` | `auto` \| `ipv4` \| `ipv6` | | | `loss_threshold_pct` | integer | Packet loss that counts as down, 1-100 (default 100). | | `payload_bytes` | integer | 16-1024 (default 56). | | `timeout_ms` | integer | Per ping, 500-10000 (default 2000). | ### Incident {#schema-incident} An incident. | Field | Type | Description | |---|---|---| | `acknowledged_at` | timestamp or null | | | `acknowledged_by` | [`UserRef`](#schema-userref) or null | `null` with `acknowledged_at` set = acknowledged via Pushover. | | `cause` | string | | | `cause_detail` | object | `RootCause` with location ids expanded to `{id, code, name}`. | | `current_status` | `up` \| `down` \| `degraded` \| `no_data` | | | `duration_s` | integer | From the first failure to resolution (or now). | | `first_failure_at` | timestamp | | | `flapping` | boolean | | | `id` | id `inc_…` | | | `monitor` | [`IncidentMonitor`](#schema-incidentmonitor) | | | `notification_count` | integer | Notification jobs of this incident with status `sent`. | | `opened_at` | timestamp | | | `reminders_sent` | integer | | | `resolution` | `recovered` \| `paused` or null | | | `resolved_at` | timestamp or null | | | `severity` | `down` \| `degraded` | Worst status reached. | | `status` | `open` \| `resolved` | | ### IncidentDetail {#schema-incidentdetail} An incident with its timeline and notifications. Every field of [`Incident`](#schema-incident), and: | Field | Type | Description | |---|---|---| | `events` | array of [`IncidentEvent`](#schema-incidentevent) | | | `notifications` | array of [`Notification`](#schema-notification) | | ### IncidentEvent {#schema-incidentevent} One timeline entry of an incident. | Field | Type | Description | |---|---|---| | `actor` | [`UserRef`](#schema-userref) or null | The user who acted, `null` for system events. | | `data` | object | Per type. | | `id` | id `iev_…` | | | `ts` | timestamp | | | `type` | `check_failed` \| `opened` \| `status_changed` \| `location_failed` \| `location_recovered` \| `no_data` \| `data_resumed` \| `flapping_started` \| `flapping_ended` \| `acknowledged` \| `reminder_sent` \| `reminders_exhausted` \| `note` \| `resolved` \| `no_contacts` \| `notification_sent` \| `notification_failed` \| `notification_suppressed` \| `notification_undelivered` | | ### IncidentMonitor {#schema-incidentmonitor} `{ "id": "mon_…", "name": "…" }`. | Field | Type | Description | |---|---|---| | `id` | id `mon_…` | | | `name` | string | | ### IncidentNoteInput {#schema-incidentnoteinput} `POST .../notes` body. | Field | Type | Description | |---|---|---| | `body` | string | 1-2000 characters. | ### Invite {#schema-invite} An open invitation. | Field | Type | Description | |---|---|---| | `created_at` | timestamp | | | `email` | string | | | `expires_at` | timestamp | | | `id` | id `inv_…` | | | `invited_by` | [`UserRef`](#schema-userref) or null | `null` if the inviter's account was deleted. | | `role` | `owner` \| `admin` \| `member` \| `viewer` | | ### Location {#schema-location} A customer-selectable location. | Field | Type | Description | |---|---|---| | `available` | boolean | Status `active` and at least one eligible probe. | | `code` | string | | | `country_code` | string | | | `id` | id `loc_…` | | | `ipv6` | boolean | | | `name` | string | | | `region` | `eu` \| `na` \| `sa` \| `as` \| `oc` \| `af` | | ### LocationsResponse {#schema-locationsresponse} `GET /locations` response. | Field | Type | Description | |---|---|---| | `locations` | array of [`Location`](#schema-location) | | | `regions` | array of [`RegionView`](#schema-regionview) | | ### MailSpec {#schema-mailspec} `imap` and `smtp`. | Field | Type | Description | |---|---|---| | `cert_expiry_warn_days` | integer or null | Degraded this many days before the certificate expires, 1-90 (default 14); `null` disables the warning; always `null` with `tls = none`. | | `host` | string | Host name or public IP address. | | `ip_family` | `auto` \| `ipv4` \| `ipv6` | | | `port` | integer | 1-65535. | | `timeout_ms` | integer | 1000-30000 (default 10000). | | `tls` | `implicit` \| `starttls` \| `none` | | | `verify_tls` | boolean | Default `true`; always `true` with `tls = none`. | ### MaintenanceWindow {#schema-maintenancewindow} A maintenance window: while it is active, the monitors it covers keep checking and record incidents, but their alerts are held. When it ends, each incident still open alerts its contacts once; incidents that ended inside the window send nothing. | Field | Type | Description | |---|---|---| | `all_monitors` | boolean | Every monitor of the org, including ones added later. | | `created_at` | timestamp | | | `created_by` | [`UserRef`](#schema-userref) or null | `null` if the member left the org. | | `ended_early` | boolean | Ended before its scheduled end. | | `ends_at` | timestamp | | | `held_incidents` | integer | Incidents whose alerts the window is holding now. | | `id` | id `mnt_…` | | | `monitor_ids` | array of id `mon_…` | The monitors covered when `all_monitors` is false. | | `name` | string | | | `starts_at` | timestamp | | | `state` | `upcoming` \| `active` \| `ended` | | ### Member {#schema-member} One member of an org. | Field | Type | Description | |---|---|---| | `joined_at` | timestamp | | | `last_active_at` | timestamp or null | Latest activity of any of the user's sessions. | | `role` | `owner` \| `admin` \| `member` \| `viewer` | | | `user` | [`MemberUser`](#schema-memberuser) | | ### MemberUser {#schema-memberuser} The user part of a member. | Field | Type | Description | |---|---|---| | `email` | string | | | `id` | id `usr_…` | | | `name` | string | | | `totp_enabled` | boolean | | ### Monitor {#schema-monitor} A monitor. Secret values are never returned. | Field | Type | Description | |---|---|---| | `contact_ids` | array of id `con_…` | | | `created_at` | timestamp | | | `degraded_after_ms` | integer or null | | | `failure_threshold` | integer | | | `id` | id `mon_…` | | | `interval_s` | integer | | | `kind` | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` | | | `labels` | array of string | | | `locations` | [`MonitorLocations`](#schema-monitorlocations) or null | `null` for `domain_expiry`: each lookup runs from one location, chosen each day; and for `heartbeat`, which runs no checks. | | `name` | string | | | `ping_url` | string or null | `heartbeat` only: the URL its job requests (`GET`, `HEAD` or `POST`) each time it runs. `null` for every other kind, and for a viewer: the URL can keep the monitor UP. | | `recovery_threshold` | integer | | | `reminder_interval_s` | integer or null | | | `run_state` | `active` \| `paused` \| `system_paused` | | | `spec` | [`MonitorSpec`](#schema-monitorspec) | The per-kind spec without the type tag (the `MonitorSpec` schema of `kind`); secrets show as `password_set` / `token_set` / `"value": null`. Fields per kind: [Monitor kinds](../monitor-kinds.md). | | `state` | [`MonitorState`](#schema-monitorstate) | | | `system_pause_reason` | `billing` \| `plan_limit` \| `org_suspended` \| `org_deleting` \| `admin` or null | | | `timeout_ms` | integer | Read-only execution budget derived from the spec. | | `updated_at` | timestamp | | | `uptime_24h` | number or null | Uptime over the last 24 h; `null` when nothing was observed. | | `version` | integer | | | `warnings` | array of string | Create and update responses only, e.g. `does_not_resolve`. | ### MonitorContactIds {#schema-monitorcontactids} `PUT .../contacts` body and response. | Field | Type | Description | |---|---|---| | `contact_ids` | array of id `con_…` | At most 100 contacts of the org; replaces the set. | ### MonitorInput {#schema-monitorinput} `POST /orgs/{org}/monitors` body. | Field | Type | Description | |---|---|---| | `contact_ids` (optional) | array of id `con_…` or null | At most 100; omitted = every `auto_attach` contact. | | `degraded_after_ms` (optional) | integer or null | `http_adv` only; 100-30000 and below the time budget; `null` = off. | | `failure_threshold` (optional) | integer or null | 1-10, default 2; for `domain_expiry` 1, the default (a second probe confirms each change instead); for `heartbeat` 1. | | `interval_s` | integer | 30, 60, 120, 300, 600, 900, 1800 or 3600, at least the plan minimum. `domain_expiry`: 86400 only. `heartbeat`: the period between pings, 60-604800 (no plan minimum). | | `kind` | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` | Immutable after create. | | `labels` (optional) | array of string | At most 10, each 1-32 characters of `[a-z0-9-_.]`. | | `locations` (optional) | [`MonitorLocationsInput`](#schema-monitorlocationsinput) or null | Required, except for `domain_expiry`, which must omit it: each of its lookups runs from one location, a different one each day; and for `heartbeat`, which runs no checks. | | `name` | string | 1-100 characters. | | `paused` (optional) | boolean | | | `recovery_threshold` (optional) | integer or null | 1-10, default 2; for `domain_expiry` and `heartbeat` 1, the default. | | `reminder_interval_s` (optional) | integer or null | `null` = no reminders; 900-86400. | | `spec` | [`MonitorSpecInput`](#schema-monitorspecinput) | The per-kind spec without the type tag: the `MonitorSpecInput` schema of `kind`; secret fields are write-only. Fields per kind: [Monitor kinds](../monitor-kinds.md). | ### MonitorLocationState {#schema-monitorlocationstate} One location of a monitor's live state. | Field | Type | Description | |---|---|---| | `code` | string | | | `error_kind` | `dns_failure` \| `blocked_address` \| `connect_timeout` \| `connection_refused` \| `connection_reset` \| `host_unreachable` \| `tls_error` \| `cert_expired` \| `timeout` \| `protocol_error` \| `status_mismatch` \| `assertion_failed` \| `too_many_redirects` \| `redirect_blocked` \| `packet_loss` \| `slow_response` \| `cert_expiring` \| `domain_expiring` \| `domain_expired` \| `domain_not_found` \| `rdap_unavailable` \| `heartbeat_missed` \| `heartbeat_failed` \| `network_unreachable` \| `icmp_unavailable` \| `resource_exhausted` \| `probe_overloaded` \| `internal` or null | | | `last_result_at` | timestamp or null | | | `latency_ms` | integer or null | | | `status` | `up` \| `down` \| `degraded` \| `no_data` | | | `substitute` | string or null | Code of the location currently standing in for this one: the location that ran the slot's newest fresh result when it was not the slot itself, or `null`. | ### MonitorLocations {#schema-monitorlocations} The location selection of a monitor. | Field | Type | Description | |---|---|---| | `codes` | array of string or null | Set when `mode = locations`. | | `mode` | `regions` \| `locations` | | | `regions` | array of `eu` \| `na` \| `sa` \| `as` \| `oc` \| `af` or null | Set when `mode = regions`. | | `resolved` | array of string | The materialised location codes, by `sort_order, code`. | ### MonitorLocationsInput {#schema-monitorlocationsinput} Location selection of a monitor (`MonitorInput.locations`). | Field | Type | Description | |---|---|---| | `codes` (optional) | array of string or null | With `mode = locations`: 2-12 location codes. | | `mode` | `regions` \| `locations` | | | `regions` (optional) | array of `eu` \| `na` \| `sa` \| `as` \| `oc` \| `af` or null | With `mode = regions`: 1-6 region codes. | ### MonitorPatch {#schema-monitorpatch} `PATCH /orgs/{org}/monitors/{mon}` body: any subset of `MonitorInput` except `kind` (JSON merge patch; `spec` is merged per RFC 7396). | Field | Type | Description | |---|---|---| | `contact_ids` (optional) | array of id `con_…` or null | | | `degraded_after_ms` (optional) | integer or null | | | `failure_threshold` (optional) | integer or null | | | `interval_s` (optional) | integer or null | | | `kind` (optional) | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` or null | Not allowed (`422 not_allowed`): the kind is immutable. | | `labels` (optional) | array of string or null | | | `locations` (optional) | [`MonitorLocationsInput`](#schema-monitorlocationsinput) or null | | | `name` (optional) | string or null | | | `paused` (optional) | boolean or null | `false` runs the same checks as `POST .../resume`. | | `recovery_threshold` (optional) | integer or null | | | `reminder_interval_s` (optional) | integer or null | | | `spec` (optional) | object or null | Merged into the stored spec (RFC 7396): any subset of the `MonitorSpecInput` schema of the monitor's kind. Fields per kind: [Monitor kinds](../monitor-kinds.md). | ### MonitorSpec {#schema-monitorspec} A monitor's `spec` in responses: the schema of its `kind`, with every field present (defaults applied, hosts and URLs normalised). Secrets of `http_adv` show as flags (`HttpAdvSpec`). The schema of the monitor's `kind`: | `kind` | Schema | |---|---| | `domain_expiry` | [`DomainExpirySpec`](#schema-domainexpiryspec) | | `heartbeat` | [`HeartbeatSpec`](#schema-heartbeatspec) | | `http` | [`HttpSpec`](#schema-httpspec) | | `http_adv` | [`HttpAdvSpec`](#schema-httpadvspec) | | `icmp` | [`IcmpSpec`](#schema-icmpspec) | | `imap` | [`MailSpec`](#schema-mailspec) | | `smtp` | [`MailSpec`](#schema-mailspec) | | `ssl_expiry` | [`SslExpirySpec`](#schema-sslexpiryspec) | | `tcp` | [`TcpSpec`](#schema-tcpspec) | ### MonitorSpecInput {#schema-monitorspecinput} A monitor's `spec` in requests: the schema of its `kind` (`imap` and `smtp` share `MailSpec`), without a type tag. Secret values of `http_adv` are sent inline (`HttpAdvSpecInput`). The schema of the monitor's `kind`: | `kind` | Schema | |---|---| | `domain_expiry` | [`DomainExpirySpec`](#schema-domainexpiryspec) | | `heartbeat` | [`HeartbeatSpec`](#schema-heartbeatspec) | | `http` | [`HttpSpec`](#schema-httpspec) | | `http_adv` | [`HttpAdvSpecInput`](#schema-httpadvspecinput) | | `icmp` | [`IcmpSpec`](#schema-icmpspec) | | `imap` | [`MailSpec`](#schema-mailspec) | | `smtp` | [`MailSpec`](#schema-mailspec) | | `ssl_expiry` | [`SslExpirySpec`](#schema-sslexpiryspec) | | `tcp` | [`TcpSpec`](#schema-tcpspec) | ### MonitorState {#schema-monitorstate} A monitor's live state. | Field | Type | Description | |---|---|---| | `last_ping_at` | timestamp or null | `heartbeat` only: when the newest ping arrived. | | `last_result_at` | timestamp or null | | | `locations` | array of [`MonitorLocationState`](#schema-monitorlocationstate) | One per configured location; empty for `domain_expiry`, whose lookups run from a different location each day (the newest result says where), and for `heartbeat`. | | `open_incident_id` | id `inc_…` or null | | | `status` | `unknown` \| `up` \| `down` \| `degraded` \| `no_data` \| `paused` | | | `status_since` | timestamp | | ### Notification {#schema-notification} One `notification_jobs` row. | Field | Type | Description | |---|---|---| | `attempts` | integer | | | `channel` | `email` \| `sms` \| `pushover` \| `discord` \| `pagerduty` \| `slack` \| `webhook` \| `voice` \| `teams` | | | `contact` | [`ContactRef`](#schema-contactref) or null | `null` for account notices sent to a user. | | `created_at` | timestamp | | | `deliveries` | array of [`DeliveryView`](#schema-deliveryview) | | | `finished_at` | timestamp or null | | | `id` | id `ntf_…` | | | `incident_id` | id `inc_…` or null | | | `kind` | `down` \| `recovered` \| `degraded` \| `flapping` \| `reminder` \| `test` \| `fallback` \| `account_notice` \| `pushover_cancel` | | | `last_error` | string or null | | | `monitor_id` | id `mon_…` or null | | | `next_attempt_at` | timestamp or null | | | `status` | `pending` \| `sending` \| `sent` \| `failed` \| `cancelled` \| `suppressed` \| `held` | | | `status_reason` | `sms_cap` \| `anomaly_brake` \| `notify_hold` \| `contact_disabled` \| `monitor_deleted` \| `monitor_paused` \| `permanent_failure` \| `gave_up` \| `maintenance` or null | | ### Org {#schema-org} An organisation as its member sees it. | Field | Type | Description | |---|---|---| | `billing` | [`OrgBilling`](#schema-orgbilling) | | | `created_at` | timestamp | | | `id` | id `org_…` | | | `my_role` | `owner` \| `admin` \| `member` \| `viewer` | | | `name` | string | | | `purge_after` | timestamp or null | Set while deleting or canceled. | | `status` | `active` \| `suspended` \| `deleting` | | | `timezone` | string | IANA name. | ### OrgBilling {#schema-orgbilling} `{plan, status}`; `status` is effective (a lapsed trial or grace period counts as `canceled`). | Field | Type | Description | |---|---|---| | `plan` | `trial` \| `starter` \| `pro` \| `business` | | | `status` | `trialing` \| `active` \| `past_due` \| `canceled` | | ### OrgEntitlements {#schema-orgentitlements} The org's effective entitlements and current usage (`GET /orgs/{org}/entitlements`): what forms and limit messages need, in either billing mode. | Field | Type | Description | |---|---|---| | `entitlements` | [`Entitlements`](#schema-entitlements) | | | `usage` | [`BillingUsage`](#schema-billingusage) | | ### OrgPatch {#schema-orgpatch} `PATCH /orgs/{org}` (merge patch). | Field | Type | Description | |---|---|---| | `name` (optional) | string or null | 1-100 characters. | | `timezone` (optional) | string or null | IANA timezone. | ### Overview {#schema-overview} The org dashboard. | Field | Type | Description | |---|---|---| | `monitors` | [`OverviewMonitors`](#schema-overviewmonitors) | | | `open_incidents` | array of [`Incident`](#schema-incident) | Newest first, at most 20. | | `recent_changes` | array of [`OverviewChange`](#schema-overviewchange) | The last 20 status periods started. | | `usage` | [`OverviewUsage`](#schema-overviewusage) | | ### OverviewChange {#schema-overviewchange} One status change. | Field | Type | Description | |---|---|---| | `at` | timestamp | | | `from` | `unknown` \| `up` \| `down` \| `degraded` \| `no_data` \| `paused` or null | `null` for a monitor's first status. | | `incident_id` | id `inc_…` or null | | | `monitor` | [`IncidentMonitor`](#schema-incidentmonitor) | | | `to` | `unknown` \| `up` \| `down` \| `degraded` \| `no_data` \| `paused` | | ### OverviewMonitors {#schema-overviewmonitors} Monitors per status; `paused` counts every monitor not running. | Field | Type | Description | |---|---|---| | `degraded` | integer | | | `down` | integer | | | `no_data` | integer | | | `paused` | integer | | | `total` | integer | | | `unknown` | integer | | | `up` | integer | | ### OverviewUsage {#schema-overviewusage} Plan usage. | Field | Type | Description | |---|---|---| | `monitors_limit` | integer | | | `monitors_used` | integer | | | `sms_credits_limit` | integer | | | `sms_credits_used` | integer | | | `sms_period_end` | timestamp | End of the current SMS period (next UTC month start). | ### PathTrace {#schema-pathtrace} A path trace. | Field | Type | Description | |---|---|---| | `completed_at` | timestamp or null | | | `created_at` | timestamp | | | `error_detail` | string or null | | | `error_kind` | string or null | `failed` only: `no_probe`, `expired`, `lost`, `invalid`, or the probe's error kind (`dns_failure`, `blocked_address`, ...). | | `hops` | array of [`TraceHop`](#schema-tracehop) | By TTL from 1 (`done` only). Responders in private, shared and reserved ranges have no `ip`. | | `id` | id `trc_…` | | | `incident_id` | id `inc_…` or null | The incident it was run for (`trigger = incident`). | | `location` | [`TraceLocation`](#schema-tracelocation) | Where it ran from. | | `mode` | `traceroute` \| `mtr` | `traceroute`: 3 probes per hop; `mtr`: 10. | | `monitor_id` | id `mon_…` | | | `port` | integer or null | The TCP port, for `tcp`. | | `protocol` | `icmp` \| `tcp` | | | `reached` | boolean or null | Whether the target answered (`done` only). | | `requested_by` | [`UserRef`](#schema-userref) or null | Who asked for it (`trigger = manual`; `null` once the user is gone). | | `status` | `pending` \| `done` \| `failed` | | | `target` | string | The monitor's target host. | | `target_ip` | string or null | The address traced to. | | `trigger` | `incident` \| `manual` | | ### PathTraceInput {#schema-pathtraceinput} `POST .../traces` body. | Field | Type | Description | |---|---|---| | `location` | string | Location code to trace from (any active location). | | `mode` (optional) | `traceroute` \| `mtr` or null | Default `mtr`. | | `protocol` (optional) | `icmp` \| `tcp` or null | Default: `tcp` to the target's port when it has one (falling back to `icmp` when no probe there can trace over TCP), else `icmp`. | ### PreviewCheckInput {#schema-previewcheckinput} `POST .../preview-check` body. | Field | Type | Description | |---|---|---| | `kind` | `icmp` \| `http` \| `http_adv` \| `tcp` \| `ssl_expiry` \| `domain_expiry` \| `imap` \| `smtp` \| `heartbeat` | | | `location` | string | Location code. | | `spec` | [`MonitorSpecInput`](#schema-monitorspecinput) | The spec as in `MonitorInput` (secrets inline). Fields per kind: [Monitor kinds](../monitor-kinds.md). | ### RegionView {#schema-regionview} A region with at least one active location. | Field | Type | Description | |---|---|---| | `code` | `eu` \| `na` \| `sa` \| `as` \| `oc` \| `af` | | | `name` | string | | ### RoleRequest {#schema-rolerequest} `{role}`. | Field | Type | Description | |---|---|---| | `role` | `owner` \| `admin` \| `member` \| `viewer` | | ### Rollup {#schema-rollup} One rollup bucket. | Field | Type | Description | |---|---|---| | `bucket_start` | timestamp | | | `checks_degraded` | integer | | | `checks_down` | integer | | | `checks_total` | integer | | | `checks_up` | integer | | | `latency_avg_ms` | number or null | | | `latency_max_ms` | integer or null | | | `latency_min_ms` | integer or null | | | `latency_p50_ms` | integer or null | | | `latency_p95_ms` | integer or null | | | `latency_p99_ms` | integer or null | | | `location` | string | | ### RollupsResponse {#schema-rollupsresponse} `GET .../rollups` response. | Field | Type | Description | |---|---|---| | `data` | array of [`Rollup`](#schema-rollup) | | | `granularity` | `hour` \| `day` | | ### Series {#schema-series} The response-time chart. | Field | Type | Description | |---|---|---| | `bands` | array of [`SeriesBand`](#schema-seriesband) | | | `from` | timestamp | | | `range` | string | | | `series` | array of [`SeriesLine`](#schema-seriesline) | | | `step_s` | integer | | | `to` | timestamp | | ### SeriesBand {#schema-seriesband} A non-`up` status period inside the range. | Field | Type | Description | |---|---|---| | `from` | timestamp | | | `status` | `unknown` \| `up` \| `down` \| `degraded` \| `no_data` \| `paused` | | | `to` | timestamp | | ### SeriesLine {#schema-seriesline} One location's latency points. | Field | Type | Description | |---|---|---| | `location` | string | | | `points` | array of array of integer or null | `[unix_s, latency_ms \| null]` for every step. | ### SslExpirySpec {#schema-sslexpiryspec} `ssl_expiry`. | Field | Type | Description | |---|---|---| | `cert_expiry_warn_days` | integer | Degraded this many days before expiry, 1-90 (default 14). | | `host` | string | Host name or public IP address. | | `ip_family` | `auto` \| `ipv4` \| `ipv6` | | | `port` | integer | 1-65535 (default 443). | | `timeout_ms` | integer | 1000-30000 (default 10000). | | `verify_tls` | boolean | Default `true`. | ### TcpSpec {#schema-tcpspec} `tcp`. | Field | Type | Description | |---|---|---| | `host` | string | Host name or public IP address. | | `ip_family` | `auto` \| `ipv4` \| `ipv6` | | | `port` | integer | 1-65535. | | `timeout_ms` | integer | 500-30000 (default 5000). | ### TestSent {#schema-testsent} `POST .../test` response. | Field | Type | Description | |---|---|---| | `notification_id` | id `ntf_…` | | ### TokenInfo {#schema-tokeninfo} `GET /token`: the calling token, its org and effective role. | Field | Type | Description | |---|---|---| | `effective_role` | `admin` \| `member` \| `viewer` | `min(token role, creator's current role)`: what the token may do. | | `org` | [`TokenOrg`](#schema-tokenorg) | | | `token` | [`ApiToken`](#schema-apitoken) | | ### TokenOrg {#schema-tokenorg} `{ "id": "org_…", "name": "…" }`. | Field | Type | Description | |---|---|---| | `id` | id `org_…` | | | `name` | string | | ### TraceHop {#schema-tracehop} One TTL of a trace. | Field | Type | Description | |---|---|---| | `addrs` | array of [`TraceHopAddr`](#schema-tracehopaddr) | Distinct responders, most replies first (at most 4); several mean load-balanced paths. | | `received` | integer | | | `rtt_avg_us` | integer or null | | | `rtt_last_us` | integer or null | | | `rtt_max_us` | integer or null | | | `rtt_min_us` | integer or null | | | `rtt_stdev_us` | integer or null | | | `sent` | integer | | | `ttl` | integer | | | `unreachable` | `net` \| `host` \| `protocol` \| `port` \| `prohibited` \| `other` or null | | ### TraceHopAddr {#schema-tracehopaddr} One responder of a hop. | Field | Type | Description | |---|---|---| | `ip` | string or null | `null` when the server withheld it: private, shared and reserved ranges (the probe host's own network) are never shown. | | `name` | string or null | Reverse DNS name, when there is one. | | `replies` | integer | Replies from this address. | ### TraceLocation {#schema-tracelocation} `{ "code": "lon1", "name": "London" }`. | Field | Type | Description | |---|---|---| | `code` | string | | | `id` | id `loc_…` | | | `name` | string | | ### UpdateMaintenanceRequest {#schema-updatemaintenancerequest} `PATCH /orgs/{org}/maintenance/{mnt}` body: rename, or move the end of a window that has not ended (to end it now, use `…/end`). The start can only change before the window starts. | Field | Type | Description | |---|---|---| | `ends_at` (optional) | timestamp or null | | | `name` (optional) | string or null | | | `starts_at` (optional) | timestamp or null | | ### UserRef {#schema-userref} `{ "id": "usr_…", "name": "…" }`. | Field | Type | Description | |---|---|---| | `id` | id `usr_…` | | | `name` | string | | ### VerifyInput {#schema-verifyinput} `POST .../verify` body. | Field | Type | Description | |---|---|---| | `code` | string | The 6-digit code. | --- # Monitor kinds A monitor is created with `POST /orgs/{org}/monitors` and a body like this: ```json { "name": "Homepage", "kind": "http", "spec": { "url": "https://www.example.com/" }, "interval_s": 60, "locations": { "mode": "regions", "regions": ["eu", "na"] }, "failure_threshold": 2, "recovery_threshold": 2, "reminder_interval_s": 3600, "labels": ["prod"], "contact_ids": ["con_01j9..."], "paused": false } ``` The common fields: | Field | Required | Rules | |---|---|---| | `name` | yes | 1-100 characters | | `kind` | yes | one of the nine kinds below; cannot change later | | `spec` | yes | the kind's fields (below); unknown fields are rejected | | `interval_s` | yes | `30`, `60`, `120`, `300`, `600`, `900`, `1800` or `3600`, and at least your plan's minimum (`below_plan_minimum`); `domain_expiry` must use `86400`; for `heartbeat`, the period between pings, `60`-`604800` (no plan minimum) | | `locations` | yes, except `domain_expiry` and `heartbeat`, which must leave it out | `{"mode": "regions", "regions": [...]}` (1-6 of `eu`, `na`, `sa`, `as`, `oc`, `af`) or `{"mode": "locations", "codes": [...]}` (2-12 codes from `GET /locations`). Regions expand to their active locations, capped at your plan's maximum per monitor; more codes than the maximum are refused (`too_many_locations`). At least 2 locations after expansion (`too_few_locations`). Exactly 2 is accepted with the warning `two_locations`: with two, an outage seen from one location can never alert. Three or more is better | | `failure_threshold` | no | 1-10, default 2: consecutive failed checks in a location before it counts as down | | `recovery_threshold` | no | 1-10, default 2: consecutive good checks before it counts as up again | | `degraded_after_ms` | no | `http_adv` only: 100-30000 and below the check's time budget; a slower response makes the monitor `degraded`. `null` (default) is off | | `reminder_interval_s` | no | 900-86400: repeat the down notification while an incident stays open and unacknowledged; `null` (default) = no reminders | | `labels` | no | at most 10, each 1-32 characters of `a-z 0-9 - _ .` | | `contact_ids` | no | contacts to notify (at most 100); omitted = every contact marked `auto_attach` | | `paused` | no | `true` creates the monitor paused | **Time budget.** Each check has a time budget, computed from its spec and returned as `timeout_ms` on the monitor. It must be at most `interval_s × 1000 − 10000` ms, so a 30-second monitor allows at most a 20-second budget (`out_of_range` on the spec's `timeout_ms` otherwise). **Hosts and URLs.** A host is a hostname or an IP address. Hostnames are stored in their ASCII (punycode) form, lower case. A hostname needs at least two labels and may not end in `localhost`, `local`, `internal`, `lan`, `home.arpa`, `localdomain`, `test`, `example`, `invalid` or `onion`. When you save, the host is resolved: if every address is private, loopback, link-local or otherwise reserved, the monitor is refused (`target_not_public`); if it does not resolve at all it is accepted with the warning `does_not_resolve`. Across your organisation, the checks of all monitors with the same host may add up to at most 120 a minute (`host_rate_exceeded`). **`ip_family`** (every kind except `domain_expiry`): `auto` (default) uses IPv6 and IPv4 as available (Happy Eyeballs; ICMP uses the first IPv4 address, or IPv6 when there is none), `ipv4` or `ipv6` only that family. `ipv6` monitors can only use locations with IPv6 (`ipv6: true` in `GET /locations`). **Outcomes.** Every check gives `up`, `down`, `degraded` or `error`. `error` means our probe could not run the check (it says nothing about your target) and never alerts. `degraded` makes a monitor `degraded` only for `http_adv`, `ssl_expiry`, `domain_expiry`, `imap` and `smtp`; for the other kinds it counts as up. A failing check gives an `error_kind`, listed per kind below. Defaults are stored with the monitor, so a later change of a default never changes your existing monitors. Responses return the spec with every default filled in. ## `http` A `GET` request. Up when the final status is 200-399 and, if `keyword` is set, the keyword appears in the first 64 KiB of the body. Up to 5 redirects are followed; TLS certificates are always verified. | Field | Required | Default | Rules | |---|---|---|---| | `url` | yes | | `http` or `https`, at most 2048 bytes, no `user:pass@`, port 1-65535; a `#fragment` is dropped | | `keyword` | no | `null` | 1-128 bytes, case-sensitive | | `timeout_ms` | no | `10000` | 1000-30000 | | `ip_family` | no | `auto` | `auto`, `ipv4`, `ipv6` | ```json { "kind": "http", "spec": { "url": "https://www.example.com/health", "keyword": "ok" } } ``` Failures: `dns_failure`, `blocked_address`, `connect_timeout`, `connection_refused`, `connection_reset`, `host_unreachable`, `tls_error`, `cert_expired`, `timeout`, `protocol_error`, `status_mismatch`, `assertion_failed` (keyword missing), `too_many_redirects`, `redirect_blocked`. ## `http_adv` Any method, headers, body, authentication, expected statuses and assertions on the response. | Field | Required | Default | Rules | |---|---|---|---| | `url` | yes | | as for `http` | | `method` | no | `GET` | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS` | | `headers` | no | `[]` | at most 32, 8 KiB together; each `{"name", "value", "secret"}` (below) | | `body` | no | `null` | at most 16 KiB; not allowed with `GET` or `HEAD`. Without a `Content-Type` header it is sent as `application/json` if it parses as JSON, else `text/plain; charset=utf-8` | | `auth` | no | `{"type": "none"}` | `{"type": "basic", "username", "password"}` or `{"type": "bearer", "token"}` | | `expected_status` | no | `["200-299"]` | 1-16 entries: `"200"`, `"200-299"` or `"2xx"`, codes 100-599 | | `assertions` | no | `[]` | at most 20 (below); all must pass | | `follow_redirects` | no | `true` | | | `max_redirects` | no | `5` | 0-10 | | `verify_tls` | no | `true` | `false` accepts any certificate chain | | `cert_expiry_warn_days` | no | `14` | 1-90: `degraded` (`cert_expiring`) when the certificate expires sooner; `null` turns the warning off | | `timeout_ms` | no | `10000` | 1000-30000 | | `body_max_bytes` | no | `65536` | 1024-262144: how much of the body is read for assertions | | `ip_family` | no | `auto` | | **Headers.** `name` is an HTTP token of at most 128 bytes; `value` at most 4096 bytes without CR, LF or NUL. Names must be unique, except `Accept`, `Cache-Control` and `X-` headers. You may not set `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`, `Keep-Alive`, `Upgrade`, `TE`, `Trailer`, `Proxy-Authorization`, `Proxy-Connection`, or `Authorization` while `auth` is not `none`. Without your own `User-Agent` the probe sends `JustMonitorIt-Probe/ (+https://justmonitorit.com/probe; loc=)`. **Secrets.** `auth.password`, `auth.token` and the `value` of a header with `"secret": true` are write-only. Responses show `"password_set": true`, `"token_set": true` and `"secret": true, "value": null` instead. When you update a monitor, omitting a secret or sending `null` keeps the stored one; `""` clears it. A secret header's name must be unique. **Assertions:** | Assertion | Passes when | |---|---| | `{"type": "body_contains", "value": "..."}` | the value (1-256 bytes, case-sensitive) is in the body | | `{"type": "body_not_contains", "value": "..."}` | the value is not in the body (only the part read, see `body_max_bytes`) | | `{"type": "body_regex", "pattern": "..."}` | the regular expression (at most 512 characters, Rust `regex` syntax) matches the body | | `{"type": "json_path", "path": "$.status", "op": "equals", "value": "ok"}` | the body is JSON and the value at `path` satisfies `op` | | `{"type": "header", "name": "content-type", "op": "contains", "value": "json"}` | the response header satisfies `op` | `json_path` paths use `$`, `.name`, `['name']` and `[index]` only (no wildcards or filters), at most 256 characters. Its `op` is `exists`, `not_exists`, `equals`, `not_equals`, `contains` (substring of a string, or an element of an array), `lt` or `gt` (numbers). `exists` and `not_exists` take no `value`; the others need one; equality is typed (`"1"` is not `1`). `header` ops are `exists`, `not_exists`, `equals`, `contains` and `regex`; the name is case-insensitive, the value comparison case-sensitive. Slow responses: set the monitor's `degraded_after_ms` to make it `degraded` (`slow_response`) when a response takes longer. ```json { "kind": "http_adv", "spec": { "url": "https://api.example.com/v1/health", "method": "GET", "headers": [ { "name": "X-Api-Key", "value": "s3cret", "secret": true } ], "expected_status": ["200"], "assertions": [ { "type": "json_path", "path": "$.status", "op": "equals", "value": "ok" }, { "type": "header", "name": "content-type", "op": "contains", "value": "application/json" } ] }, "degraded_after_ms": 1500 } ``` Failures: as `http`, plus `cert_expiring` (degraded) and `slow_response` (degraded). ## `tcp` Up when a TCP connection to the port opens. Nothing is sent. | Field | Required | Default | Rules | |---|---|---|---| | `host` | yes | | hostname or IP address | | `port` | yes | | 1-65535 | | `timeout_ms` | no | `5000` | 500-30000 | | `ip_family` | no | `auto` | | ```json { "kind": "tcp", "spec": { "host": "db.example.com", "port": 5432 } } ``` Failures: `dns_failure`, `blocked_address`, `connect_timeout`, `connection_refused`, `host_unreachable`. ## `icmp` Ping. Sends `count` echo requests; down when the share lost reaches `loss_threshold_pct` (the default 100 means down only when every echo is lost). | Field | Required | Default | Rules | |---|---|---|---| | `host` | yes | | hostname or IP address | | `count` | no | `3` | 1-10 | | `interval_ms` | no | `500` | 200-1000, between echo requests | | `timeout_ms` | no | `2000` | 500-10000, wait after the last request | | `payload_bytes` | no | `56` | 16-1024 | | `loss_threshold_pct` | no | `100` | 1-100 | | `ip_family` | no | `auto` | | The time budget is `3000 + (count − 1) × interval_ms + timeout_ms`. ```json { "kind": "icmp", "spec": { "host": "203.0.113.10", "count": 5, "loss_threshold_pct": 60 } } ``` Failures: `packet_loss`, `host_unreachable`, `dns_failure`, `blocked_address`. ## `ssl_expiry` Connects, completes a TLS handshake and checks the certificate. Down if the handshake fails or the certificate has expired; `degraded` (`cert_expiring`) when it expires within `cert_expiry_warn_days`. Use it for TLS services that are not websites, or to watch a certificate on its own. | Field | Required | Default | Rules | |---|---|---|---| | `host` | yes | | hostname (also sent as SNI) or IP address | | `port` | no | `443` | 1-65535 | | `cert_expiry_warn_days` | no | `14` | 1-90 | | `verify_tls` | no | `true` | `false` checks only the expiry of whatever certificate is served | | `timeout_ms` | no | `10000` | 1000-30000 | | `ip_family` | no | `auto` | | ```json { "kind": "ssl_expiry", "spec": { "host": "www.example.com", "cert_expiry_warn_days": 21 } } ``` Failures: transport failures as `tcp`, `tls_error`, `cert_expired`, `timeout`, `protocol_error`; `cert_expiring` (degraded). ## `domain_expiry` Looks up the domain's registration at its registry over RDAP, once a day (`interval_s` must be `86400`). Down when the registration has expired (`domain_expired`) or the domain is not registered (`domain_not_found`); `degraded` (`domain_expiring`) within `expiry_warn_days`. If the registry cannot be reached, the result is `error` (`rdap_unavailable`) and you are not alerted. | Field | Required | Default | Rules | |---|---|---|---| | `domain` | yes | | the registered domain (`example.com`), not an IP address; its TLD must have an RDAP server (`unsupported_tld` otherwise, for example `ch` and `de`) | | `expiry_warn_days` | no | `30` | 1-365 | | `timeout_ms` | no | `10000` | 1000-30000 | ```json { "kind": "domain_expiry", "interval_s": 86400, "spec": { "domain": "example.com" } } ``` ## `imap` and `smtp` Opens a session with a mail server, without logging in or sending mail: the greeting, `EHLO` (SMTP) or `CAPABILITY` (IMAP), optionally `STARTTLS`, then a polite goodbye. With TLS, the certificate is checked as for `ssl_expiry`. | Field | Required | Default | Rules | |---|---|---|---| | `host` | yes | | | | `port` | yes | | 1-65535 (IMAP usually 993 or 143; SMTP 25, 465 or 587) | | `tls` | yes | | `implicit` (TLS from the start), `starttls` or `none` | | `verify_tls` | no | `true` | forced to `true` with `tls: none` | | `cert_expiry_warn_days` | no | `14` | 1-90, or `null` for no warning; forced to `null` with `tls: none` | | `timeout_ms` | no | `10000` | 1000-30000 | | `ip_family` | no | `auto` | | ```json { "kind": "smtp", "spec": { "host": "mx.example.com", "port": 587, "tls": "starttls" } } ``` SMTP on ports 25 and 465 runs only from probes whose provider allows outbound mail ports; where there is none, another location stands in. Failures: transport and TLS failures as `ssl_expiry`, `timeout`, `protocol_error` (detail `greeting`, `ehlo`, `starttls_unsupported`, `starttls_refused`, `capability` or `line_too_long`); `cert_expiring` (degraded). ## `heartbeat` Checks nothing itself: your job (a cron script, a backup, a queue worker) requests the monitor's ping URL each time it runs, and the monitor goes `down` (`heartbeat_missed`) when no ping arrives within `interval_s` plus `grace_s`. The next ping brings it back `up`. A new heartbeat does not alert until its first ping. `interval_s` is the period between pings (`60`-`604800`); leave out `locations`, and the thresholds are `1`. | Field | Required | Default | Rules | |---|---|---|---| | `grace_s` | no | `300` | 60-604800: how late a ping may be | ```json { "kind": "heartbeat", "interval_s": 86400, "spec": { "grace_s": 1800 } } ``` The response's `ping_url` is the URL to request, with `GET`, `HEAD` or `POST` (a body is ignored), for example at the end of a crontab line: ``` 0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://dash.justmonitorit.com/ping/... ``` To report a failed run, request the same URL followed by `/fail`: the monitor goes `down` at once (`heartbeat_failed`), and the next successful ping brings it back `up`: ``` 0 3 * * * if /usr/local/bin/backup.sh; then curl -fsS -m 10 --retry 5 -o /dev/null https://dash.justmonitorit.com/ping/...; else curl -fsS -m 10 --retry 5 -o /dev/null https://dash.justmonitorit.com/ping/.../fail; fi ``` `GET /orgs/{org}/monitors/{mon}/pings` lists the newest pings (100 are kept): when each arrived, `success` or `fail`, the method, the client's address and user agent. Only members and above see `ping_url`; a viewer gets `null`. It answers `200` once the ping counted, `404` for an unknown URL, and `429` or `503` (with `Retry-After`) when it should be tried again. At most 60 pings a minute per URL count. The newest ping is the monitor's `state.last_ping_at`. Anyone with the URL can ping the monitor, so keep it private; to change it, delete the monitor and create a new one. ## Trying a spec first `POST /orgs/{org}/monitors/preview-check` runs a spec once from one location and returns the result without creating anything: ```json { "kind": "http", "spec": { "url": "https://www.example.com/" }, "location": "lon1" } ``` It applies the same validation as creating a monitor. A `heartbeat` has nothing to try (`not_allowed`). --- # Webhooks A webhook contact posts a JSON document to an HTTPS endpoint of yours each time it is alerted: your own service, or an automation tool that accepts webhooks. Attach it to monitors like any other contact. ## Setting one up 1. Add a contact of kind **Webhook** with the endpoint's URL, in the web app or with the API: ```json { "name": "Ops webhook", "kind": "webhook", "secret": { "url": "https://hooks.example.com/justmonitorit?token=7f3c..." } } ``` The URL must be `https://` on a domain name (not an IP address). Put a token of your choosing in its path or query and check it on your side: it is how your endpoint tells our requests from anyone else's. The URL is stored encrypted and never shown again. 2. Prove that you control the domain. The new contact is `unverified`, and its `config` holds a DNS record to publish: ```json "config": { "host": "hooks.example.com", "txt_name": "_justmonitorit.hooks.example.com", "txt_value": "justmonitorit-webhook-verification=0123456789abcdef0123456789abcdef" } ``` Add a TXT record with that name and value at your DNS provider. The value is your organisation's and is the same for every webhook, so one record covers every endpoint on that host. The record goes on the exact host of the URL; a record on a parent domain does not count. 3. Press **Check again** on the contact, or call `POST /orgs/{org}/contacts/{id}/verify/send`. Once the record is found the contact is `active`. Until then it is sent nothing, and the check answers `422` with `webhook_domain_unverified` and the record it looked for. DNS changes can take a few minutes to appear. If the host already has your record (another webhook on it), a new webhook is `active` at once. Changing the URL to another host checks that host. With Terraform, the contact's `webhook_txt_name` and `webhook_txt_value` attributes let your DNS provider's resource publish the record ([Terraform and Pulumi](terraform.md)). ## What we send `POST` to your URL with `Content-Type: application/json`, `User-Agent: JustMonitorIt-Webhook/` and `X-JMI-Delivery: `. One request carries every alert that is due for the contact at that moment (alerts within a few seconds of each other are grouped): ```json { "version": 1, "type": "alert", "delivery_id": "dlv_01j9...", "sent_at": "2026-10-06T14:07:14Z", "org": { "id": "org_01j9...", "name": "Acme" }, "contact": { "id": "con_01j9...", "name": "Ops webhook" }, "events": [ { "id": "ntf_01j9...", "event": "down", "state": "DOWN", "at": "2026-10-06T14:07:13Z", "summary": "DOWN: API: Connection timed out after 10 s", "monitor": { "id": "mon_01j9...", "name": "API", "kind": "http", "target": "https://api.example.com/health", "url": "https://dash.justmonitorit.com/monitors/mon_01j9...?org=org_01j9..." }, "incident": { "id": "inc_01j9...", "opened_at": "2026-10-06T14:03:01Z", "url": "https://dash.justmonitorit.com/incidents/inc_01j9...?org=org_01j9..." }, "status": "down", "previous_status": "up", "error": { "kind": "connect_timeout", "summary": "Connection timed out after 10 s", "status_code": null }, "degraded": null, "locations": { "failing": [{ "code": "lon1", "name": "London" }, { "code": "fra1", "name": "Frankfurt" }], "total": 3 }, "downtime_s": null, "reminder_n": null, "flap_transitions": null } ] } ``` - `type` is `alert`, or `test` for **Send test alert** (with a `message` and no events). - `event` is `down`, `degraded` (slow, or a certificate or domain close to expiry; see `degraded.reason`), `recovered`, `flapping` or `reminder` (still down). `state` is the short word for it: `DOWN`, `SLOW`, `CERT`, `DOMAIN`, `UP` or `FLAPPING`. - `downtime_s` is set for `recovered` and `reminder`, `reminder_n` for `reminder`, `flap_transitions` for `flapping`. `degraded` holds `reason`, `latency_ms`, `degraded_after_ms`, `tls_days_left` and `domain_days_left`. - New fields may be added to version 1; ignore the ones you do not know. ## Your response Answer with any `2xx` status, within 15 seconds. We do not follow redirects. - `429`, `5xx`, a timeout or a refused connection: we retry with backoff from 10 seconds up to 30 minutes (a `Retry-After` header is honoured), for up to 6 hours or 24 attempts. A retry sends the same events again, so use each event's `id` to drop ones you already have; `delivery_id` changes on each attempt. - `410 Gone`: the contact is disabled. **Check again** turns it back on. - Other `4xx`, or a redirect: that alert is not retried. Five such failures in a row disable the contact, and the organisation's owners and admins get an email. The contact page lists every delivery with its status code and the start of your response. --- # Examples Complete recipes against the REST API. They assume a token in the environment variable `JMI_TOKEN` ([Authentication](authentication.md)) and use the token's own organisation, found with `GET /token`. ```sh export JMI_TOKEN=jmi_... export JMI_API=https://dash.justmonitorit.com/api/v1 ``` The shell examples use [`jq`](https://jqlang.org/), the Python ones [`requests`](https://requests.readthedocs.io/) and the JavaScript ones the `fetch` and `crypto` built into Node.js 20 and later. ## Who am I? ```sh curl -s "$JMI_API/token" -H "Authorization: Bearer $JMI_TOKEN" | jq ORG=$(curl -s "$JMI_API/token" -H "Authorization: Bearer $JMI_TOKEN" | jq -r .org.id) ``` ## A small client The other Python recipes use this helper. It raises on errors with the problem `code`, retries `429` and `503` after `Retry-After`, and follows cursors. ```python import os, time, uuid import requests API = os.environ.get("JMI_API", "https://dash.justmonitorit.com/api/v1") session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['JMI_TOKEN']}" class ApiError(Exception): def __init__(self, problem): super().__init__(f"{problem['status']} {problem['code']}: {problem.get('detail')}") self.problem = problem def call(method, path, **kw): for attempt in range(5): r = session.request(method, API + path, timeout=60, **kw) if r.status_code in (429, 503) and attempt < 4: time.sleep(int(r.headers.get("Retry-After", "2"))) continue if r.status_code >= 400: raise ApiError(r.json()) return r.json() if r.content else None def pages(path, **params): """Every item of a cursor-paginated list.""" while True: page = call("GET", path, params=params) yield from page["data"] if not page["next_cursor"]: return params["cursor"] = page["next_cursor"] ORG = call("GET", "/token")["org"]["id"] ``` ## List the monitors that are down ```sh curl -s "$JMI_API/orgs/$ORG/monitors?status=down,degraded" \ -H "Authorization: Bearer $JMI_TOKEN" \ | jq -r '.data[] | "\(.state.status)\t\(.name)\t\(.state.status_since)"' ``` ```python for m in pages(f"/orgs/{ORG}/monitors", status="down,degraded", limit=500): print(m["state"]["status"], m["name"], m["state"]["status_since"]) ``` `status` accepts a comma-separated list of `unknown`, `up`, `down`, `degraded`, `no_data` and `paused`. Other filters: `kind`, `label`, `run_state`, `contact_id` and `q` (a substring of the name or target). ## Create HTTP monitors from a list Each monitor is created with an `Idempotency-Key`, so running the script again within 24 hours (after a timeout, say) does not create duplicates. The key is derived from the URL; change the namespace to create them again on purpose. ```python URLS = [ "https://www.example.com/", "https://api.example.com/health", "https://status.example.com/", ] NAMESPACE = uuid.UUID("6f1c2f40-0b7e-4c43-9a51-7d1b2f6a9e10") for url in URLS: body = { "name": url.split("//", 1)[1].rstrip("/"), "kind": "http", "spec": {"url": url, "timeout_ms": 10000}, "interval_s": 60, "locations": {"mode": "regions", "regions": ["eu", "na"]}, "labels": ["prod"], } key = str(uuid.uuid5(NAMESPACE, url)) try: m = call("POST", f"/orgs/{ORG}/monitors", json=body, headers={"Idempotency-Key": key}) print("created", m["id"], m["name"], m["warnings"]) except ApiError as e: # 422: the problem lists every invalid field for err in e.problem.get("errors") or []: print(url, err["pointer"], err["code"], err["message"]) if e.problem["code"] == "plan_limit": print("plan limit reached:", e.problem.get("limit")) break ``` The same in JavaScript: ```js const API = process.env.JMI_API ?? "https://dash.justmonitorit.com/api/v1"; const headers = { Authorization: `Bearer ${process.env.JMI_TOKEN}` }; async function call(method, path, body, extra = {}) { const res = await fetch(API + path, { method, headers: { ...headers, ...(body ? { "Content-Type": "application/json" } : {}), ...extra }, body: body ? JSON.stringify(body) : undefined, }); const data = res.status === 204 ? null : await res.json(); if (!res.ok) throw Object.assign(new Error(`${data.status} ${data.code}`), { problem: data }); return data; } const { org } = await call("GET", "/token"); const monitor = await call("POST", `/orgs/${org.id}/monitors`, { name: "Checkout API", kind: "http_adv", spec: { url: "https://api.example.com/v1/checkout/health", expected_status: ["200"], assertions: [{ type: "json_path", path: "$.status", op: "equals", value: "ok" }], }, interval_s: 60, locations: { mode: "locations", codes: ["lon1", "fra1", "nyc1"] }, degraded_after_ms: 2000, }, { "Idempotency-Key": crypto.randomUUID() }); console.log(monitor.id, monitor.timeout_ms); ``` The `spec` of each kind is in [Monitor kinds](monitor-kinds.md). To test a spec without creating a monitor, `POST /orgs/{org}/monitors/preview-check` with `{"kind", "spec", "location"}` runs it once and returns the result. ## Pause monitors during a deploy Pause every monitor labelled `prod` before a deploy and resume them afterwards. A paused monitor sends no alerts, and pausing resolves an open incident. ```sh #!/bin/sh set -eu auth="Authorization: Bearer $JMI_TOKEN" ORG=$(curl -sf "$JMI_API/token" -H "$auth" | jq -r .org.id) ids=$(curl -sf "$JMI_API/orgs/$ORG/monitors?label=prod&run_state=active&limit=500" -H "$auth" \ | jq -r '.data[].id') for id in $ids; do curl -sf -X POST "$JMI_API/orgs/$ORG/monitors/$id/pause" -H "$auth" > /dev/null done status=0 ./deploy.sh || status=$? # your deploy; resume even if it fails for id in $ids; do curl -sf -X POST "$JMI_API/orgs/$ORG/monitors/$id/resume" -H "$auth" > /dev/null \ || echo "could not resume $id" >&2 done exit "$status" ``` Pausing and resuming need a `member` token. Only the monitors that were running are resumed, so monitors you paused by hand stay paused. Resuming can fail with `403 plan_limit` when the plan no longer has room for the monitor. ## Acknowledge open incidents Acknowledging stops reminders. With a token the acknowledgement is recorded as yours. ```js const { org } = await call("GET", "/token"); let cursor = null; do { const q = new URLSearchParams({ status: "open", limit: "200", ...(cursor ? { cursor } : {}) }); const page = await call("GET", `/orgs/${org.id}/incidents?${q}`); for (const inc of page.data) { if (inc.acknowledged_at) continue; try { await call("POST", `/orgs/${org.id}/incidents/${inc.id}/acknowledge`); await call("POST", `/orgs/${org.id}/incidents/${inc.id}/notes`, { body: "Acknowledged by the on-call bot; investigating.", }); console.log("acknowledged", inc.id, inc.monitor.name, inc.cause); } catch (e) { if (e.problem?.code !== "incident_resolved") throw e; // it recovered meanwhile } } cursor = page.next_cursor; } while (cursor); ``` ## Export an uptime report as CSV Uptime for the last 30 days of every monitor. `uptime_ratio` is `null` when nothing was observed (a monitor paused for the whole window, say). ```python import csv, sys out = csv.writer(sys.stdout) out.writerow(["monitor", "kind", "uptime_30d_pct", "down_minutes", "degraded_minutes"]) for m in pages(f"/orgs/{ORG}/monitors", limit=500): u = call("GET", f"/orgs/{ORG}/monitors/{m['id']}/uptime", params={"windows": "30d"})["data"][0] pct = "" if u["uptime_ratio"] is None else f"{u['uptime_ratio'] * 100:.3f}" out.writerow([m["name"], m["kind"], pct, round(u["down_s"] / 60), round(u["degraded_s"] / 60)]) ``` For any period, use `?from=2026-09-01T00:00:00.000Z&to=2026-10-01T00:00:00.000Z` instead of `windows`. History older than your plan's retention is not counted. A large organisation makes one request per monitor: stay under 600 reads a minute. ## Attach contacts to monitors Notify the on-call phone for every monitor labelled `prod`, keeping each monitor's other contacts: ```sh auth="Authorization: Bearer $JMI_TOKEN" CON=$(curl -sf "$JMI_API/orgs/$ORG/contacts?kind=sms" -H "$auth" \ | jq -r '.data[] | select(.name == "On-call phone") | .id') curl -sf "$JMI_API/orgs/$ORG/monitors?label=prod&limit=500" -H "$auth" \ | jq -c '.data[] | {id, contact_ids}' \ | while read -r m; do id=$(echo "$m" | jq -r .id) body=$(echo "$m" | jq -c --arg con "$CON" '{contact_ids: ((.contact_ids + [$con]) | unique)}') curl -sf -X PUT "$JMI_API/orgs/$ORG/monitors/$id/contacts" -H "$auth" \ -H "Content-Type: application/json" -d "$body" > /dev/null done ``` `PUT .../contacts` replaces the whole set. The reverse also exists: `PUT /orgs/{org}/contacts/{con}/monitors` with `{"monitor_ids": [...]}` sets every monitor a contact is notified for. A new contact is created with `POST /orgs/{org}/contacts`; email and SMS contacts must then be verified with the code they receive (`POST .../contacts/{con}/verify` with `{"code": "123456"}`) before they get alerts: ```sh curl -s -X POST "$JMI_API/orgs/$ORG/contacts" -H "$auth" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"name": "Ops mailbox", "kind": "email", "config": {"address": "ops@example.com"}, "notify_on": ["down", "recovered"], "auto_attach": true}' ``` ## Read recent check results The last hour of failed checks of one monitor, with the location and the error: ```sh FROM=$(date -u -v-1H +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S.000Z) curl -s "$JMI_API/orgs/$ORG/monitors/$MON/results?outcome=down&from=$FROM&limit=1000" \ -H "Authorization: Bearer $JMI_TOKEN" \ | jq -r '.data[] | "\(.ts) \(.location) \(.error_kind) \(.error_detail // "")"' ``` ## GraphQL and MCP - The same data in one request (monitors with their uptime and open incidents) is easier with [GraphQL](graphql.md). - To let an AI assistant read your monitors and incidents, or act on them, connect it to the [MCP server](mcp.md) with a token; a `viewer` token keeps it read-only. --- # Terraform, OpenTofu and Pulumi Manage monitors and contacts as code. The JustMonitorIt provider works with Terraform (1.0 or later) and OpenTofu, and its Pulumi version uses the same code. It talks to the [REST API](rest.md) with an API token, so the same roles, validation, plan limits and audit log apply. ## Set up 1. **Create a token** with the `member` role (or `admin`) under **Settings → API tokens**. See [Authentication](authentication.md). 2. **Require the provider.** It is installed from JustMonitorIt's own registry, so the source starts with `dash.justmonitorit.com`: ```hcl terraform { required_providers { justmonitorit = { source = "dash.justmonitorit.com/justmonitorit/justmonitorit" version = "~> 0.1" } } } provider "justmonitorit" {} ``` 3. **Pass the token** in the `JUSTMONITORIT_API_TOKEN` environment variable (or `api_token` in the provider block, from a sensitive variable), then run `terraform init`. The provider works on the organisation of its token. `terraform init` checks each download against a checksum list signed with the registry key (ID `160A38870F4B46E0`, fingerprint `356C A0E2 8CBD 74A8 2B7C AE77 160A 3887 0F4B 46E0`; the public key is at [`/_registry/signing-key.asc`](https://dash.justmonitorit.com/_registry/signing-key.asc)) and records the checksums in `.terraform.lock.hcl`. ## Monitors Set exactly one kind: `http`, `http_adv`, `tcp`, `icmp`, `ssl_expiry`, `domain_expiry`, `imap`, `smtp` or `heartbeat`. The fields of each are those of the [monitor kinds](monitor-kinds.md), with the same defaults. ```hcl resource "justmonitorit_contact" "ops" { name = "Ops team" email = { address = "ops@example.com" } } resource "justmonitorit_monitor" "homepage" { name = "Homepage" interval_s = 60 locations = { regions = ["eu"] } http = { url = "https://example.com/", keyword = "Welcome" } labels = ["web", "production"] contact_ids = [justmonitorit_contact.ops.id] } resource "justmonitorit_monitor" "api" { name = "Status API" interval_s = 30 locations = { regions = ["eu", "na"] } degraded_after_ms = 2000 http_adv = { url = "https://api.example.com/health" headers = [{ name = "X-Api-Key", secret_value = var.health_key }] assertions = [ { type = "json_path", path = "$.status", op = "equals", json_value = jsonencode("ok") }, ] } contact_ids = [justmonitorit_contact.ops.id] } resource "justmonitorit_monitor" "domain" { name = "example.com registration" domain_expiry = { domain = "example.com", expiry_warn_days = 45 } } resource "justmonitorit_monitor" "backup" { name = "Nightly backup" interval_s = 86400 heartbeat = { grace_s = 1800 } } output "backup_ping_url" { value = justmonitorit_monitor.backup.ping_url sensitive = true } ``` - **Locations**: `locations = { regions = [...] }` (every location in those regions) or `locations = { codes = [...] }` (2-12 locations; the `justmonitorit_locations` data source lists them). `domain_expiry` monitors leave `locations` and `interval_s` out: they look the domain up once a day. - **Heartbeats**: `interval_s` is the period between the job's pings (60-604800 seconds), and there are no `locations`. The computed `ping_url` (sensitive: anyone who has it can ping the monitor) is what the job requests; followed by `/fail` it reports a failed run. In Pulumi it is the `pingUrl` output. - **Changing the kind** replaces the monitor (a new id, new history). - **Removing an attribute** puts its default back. - **`cert_expiry_warn_days = 0`** turns the certificate warning off (the API's `null`). ## Contacts Set exactly one channel: `email`, `sms`, `pushover`, `discord`, `pagerduty`, `slack` or `webhook`. ```hcl resource "justmonitorit_contact" "pagerduty" { name = "PagerDuty" pagerduty = { routing_key = var.pagerduty_routing_key, region = "eu" } notify_on = ["down", "recovered", "degraded"] } ``` Email and SMS contacts start `unverified`: the recipient confirms them before alerts are sent, as when you add them in the web app. Creating them sends a code, and an organisation can send 10 codes a day, so a configuration with more than 10 new email or SMS contacts needs a second `apply` the next day. A `webhook` contact stays `unverified` until its host has a TXT record that proves you control it. The contact's `webhook_txt_name` and `webhook_txt_value` give the record, so your DNS provider's resource can publish it in the same configuration: ```hcl resource "justmonitorit_contact" "webhook" { name = "Ops webhook" webhook = { url = "https://hooks.example.com/justmonitorit?token=${var.webhook_token}" } } # For example with Cloudflare: resource "cloudflare_dns_record" "justmonitorit" { zone_id = var.zone_id name = justmonitorit_contact.webhook.webhook_txt_name type = "TXT" content = "\"${justmonitorit_contact.webhook.webhook_txt_value}\"" ttl = 300 } ``` Then press **Check again** on the contact in the web app (or `POST .../contacts/{id}/verify/send`) once the record is live. Attach contacts with a monitor's `contact_ids`. Leave `contact_ids` out and a new monitor gets every contact with **auto attach**, and Terraform leaves its contacts alone afterwards. `paused` works the same way: set it to manage pausing in code, leave it out to pause and resume in the web app. ## How it behaves - **Secrets** (`basic_auth.password`, `bearer_token`, `secret_value`, `user_key`, `webhook_url`, `routing_key`) are write-only in the API: the provider keeps them in the state, marked sensitive. Protect your state as you protect the secrets. A secret changed in the web app is not detected. - **Values the API normalises**, such as `https://Example.com` (stored as `https://example.com/`), stay as you wrote them in the state; a later change in the web app still shows as a difference. - **Changes made elsewhere** show in `terraform plan`, and `apply` puts the configuration back. - **Concurrent edits**: an update fails, without changing anything, if the monitor or contact was changed after Terraform last read it. Run `apply` again. - **Rate limits**: requests over the per-minute write limit wait and retry for up to 5 minutes, so large applies just take longer. A longer limit fails at once and says when to retry. - **Import**: `terraform import justmonitorit_monitor.homepage mon_…` (or `con_…` for a contact). Set any secrets in the configuration and apply once after importing. Data sources: `justmonitorit_locations`, `justmonitorit_monitor` (with its live `status` and `uptime_24h`) and `justmonitorit_contact`. ## OpenTofu Use the same `source`. OpenTofu checks the same signature. ## Pulumi The Pulumi provider has the same resources, with Pulumi names (`justmonitorit:Monitor`, `justmonitorit:Contact`) and camelCase properties (`intervalS`, `httpAdv`, `secretValue`). Install its plugin once, then use it from a YAML program: ```sh pulumi plugin install resource justmonitorit v0.1.0 \ --server https://dash.justmonitorit.com/_registry/pulumi pulumi config set --secret justmonitorit:apiToken jmi_... ``` ```yaml name: monitoring runtime: yaml resources: homepage: type: justmonitorit:Monitor properties: name: Homepage intervalS: 60 locations: regions: [eu] http: url: https://example.com/ ``` For TypeScript, Python, Go or C#, generate a local SDK from the Terraform provider: `pulumi package add terraform-provider dash.justmonitorit.com/justmonitorit/justmonitorit`. --- # GraphQL API The GraphQL API reads and changes the same data as the REST API, with the same rules: roles, plan limits, validation, rate limits and the audit log are those of the REST operations it runs for you. Use it when you want one request to fetch exactly the fields you need: a monitor with its open incident and last results, or every down monitor with its contacts. - **Endpoint:** `POST https://dash.justmonitorit.com/api/v1/graphql` - **Schema (SDL):** `GET https://dash.justmonitorit.com/api/v1/graphql/schema.graphql` (public), also in [generated/schema.graphql](generated/schema.graphql) - **Request body:** `{"query": "...", "variables": {...}, "operationName": "..."}` as `Content-Type: application/json` - **Response:** `200` with `{"data": ..., "errors": [...]}` There is no GET execution, no batching (one operation per request) and no subscriptions. ## Authentication Send an API token in the `Authorization` header: ``` Authorization: Bearer jmi_… ``` A token belongs to one organisation, so `organization` and every mutation default to that organisation; you never need to pass its id. The token's role decides what it may do: `viewer` tokens read, `member` tokens also change monitors, incidents and contacts, `admin` tokens also read the members list and the audit log. See [Authentication](authentication.md) for how to create a token. Without a valid token the endpoint answers HTTP `401` with a problem body (not a GraphQL response), so a misconfigured client fails loudly: ```json { "type": "https://justmonitorit.com/problems/unauthenticated", "title": "Not signed in", "status": 401, "code": "unauthenticated", "detail": "The API token is missing, unknown, expired or revoked.", "request_id": "…" } ``` In the web app's playground, requests run as your signed-in session instead. With a session you must name the organisation: `organization(id: "org_…")` and `orgId: "org_…"` on mutations. ## A first query ```bash curl -s https://dash.justmonitorit.com/api/v1/graphql \ -H "Authorization: Bearer $JMI_TOKEN" \ -H "Content-Type: application/json" \ -d '{"query": "{ token { org { name } effectiveRole } organization { overview { monitors { total up down } } } }"}' ``` ```json { "data": { "token": { "org": { "name": "Acme" }, "effectiveRole": "VIEWER" }, "organization": { "overview": { "monitors": { "total": 48, "up": 46, "down": 1 } } } } } ``` The same from JavaScript (Node 18+ or a browser-side tool, never a public web page: the token is a secret): ```js const res = await fetch("https://dash.justmonitorit.com/api/v1/graphql", { method: "POST", headers: { Authorization: `Bearer ${process.env.JMI_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: `query Down { organization { monitors(status: [DOWN, DEGRADED]) { nodes { id name state { status statusSince } openIncident { id cause } } } } }`, }), }); const { data, errors } = await res.json(); if (errors) throw new Error(errors.map((e) => e.message).join("; ")); console.log(data.organization.monitors.nodes); ``` And from Python with `requests`: ```python import os, requests query = """ query($id: ID!) { organization { monitor(id: $id) { name uptime(windows: [LAST_24H, LAST_30D]) { window uptimeRatio } results(first: 5) { nodes { ts location outcome latencyMs errorKind } } } } } """ r = requests.post( "https://dash.justmonitorit.com/api/v1/graphql", headers={"Authorization": f"Bearer {os.environ['JMI_TOKEN']}"}, json={"query": query, "variables": {"id": "mon_01j9zq3k8e5ffv0j2n6m7w4x1c"}}, timeout=30, ) r.raise_for_status() body = r.json() print(body.get("errors") or body["data"]) ``` ## Conventions - **Names** are the REST names in camelCase: `interval_s` is `intervalS`, `status_since` is `statusSince`. - **Enums** are the REST values in capitals: `no_data` is `NO_DATA`, `http_adv` is `HTTP_ADV`. - **Ids** are the REST ids (`mon_…`, `inc_…`, `con_…`), of type `ID`. - **`DateTime`** is an RFC 3339 UTC string with milliseconds, `2026-10-06T12:00:00.000Z`. - **`JSON`** holds free-form objects exactly as REST returns them: a monitor's `spec`, an incident's `causeDetail`, an audit entry's `data`, `entitlements`. ## Reading The roots are: | Field | Returns | |---|---| | `token` | The calling token, its organisation and effective role (`null` for a session) | | `organization(id: ID)` | An organisation; `id` defaults to the token's | | `locations` | Active check locations and regions (also without a token) | Everything else hangs off `Organization`: | Field | Notes | |---|---| | `overview` | Monitor counts by status, open incidents, recent changes, usage | | `monitors(status, kind, label, runState, contactId, search, sort, direction, first, after)` | Filters as in `GET /orgs/{org}/monitors` | | `monitor(id)` | One monitor | | `incidents(status, monitorId, severity, from, to, first, after)` | Newest first | | `incident(id)` | One incident; `events` and `notifications` are its timeline | | `contacts(kind, status, first, after)`, `contact(id)` | Notification targets | | `notifications(contactId, incidentId, monitorId, status, kind, first, after)`, `notification(id)` | The delivery log | | `members` | Members and roles | | `auditLog(action, actor, first, after)` | Admin role | | `entitlements` | Plan limits and usage (`JSON`) | A `Monitor` adds `results`, `uptime`, `timeline`, `series`, `incidents`, `openIncident` and `contacts`. An `Incident` adds `monitor` (the full monitor), `monitorRef` (its id and name only, cheaper), `events` and `notifications`. A `Contact` adds `monitors` and `notifications`. ```graphql query Overview { organization { name monitors(status: [DOWN], first: 20) { nodes { id name kind state { status statusSince locations { code status latencyMs errorKind } } openIncident { id cause openedAt acknowledgedAt events { ts type data } } contacts { name kind status } } } } } ``` ### Pagination Lists are connections over the REST cursors: pass `first` (the REST `limit`: default 50; at most 500 for monitors, 1,000 for results, 200 otherwise) and, for the next page, `after: pageInfo.endCursor`. ```graphql query Page($after: String) { organization { incidents(status: RESOLVED, first: 100, after: $after) { nodes { id monitorRef { name } openedAt resolvedAt durationS } pageInfo { endCursor hasNextPage } } } } ``` Repeat while `hasNextPage` is `true`. Cursors are opaque; do not build them yourself. ## Changing things Mutations mirror the REST writes. Each takes `orgId` (optional with a token). Creates accept `idempotencyKey` (8-64 characters `[A-Za-z0-9_-]`): retrying with the same key returns the first result instead of creating a second monitor. Updates accept `ifMatch`, the `version` you read; if someone changed the object since, the mutation fails with `precondition_failed` and you should read it again. | Mutation | Role | |---|---| | `createMonitor(input, idempotencyKey)`, `updateMonitor(id, patch, ifMatch)`, `deleteMonitor(id, ifMatch)` | member | | `pauseMonitor(id)`, `resumeMonitor(id)`, `setMonitorContacts(id, contactIds)` | member | | `acknowledgeIncident(id)`, `addIncidentNote(id, body)` | member | | `createContact(input, idempotencyKey)`, `updateContact(id, patch, ifMatch)`, `deleteContact(id, ifMatch)` | member | | `enableContact(id)`, `disableContact(id)`, `setContactMonitors(id, monitorIds)` | member | | `sendContactVerification(id)`, `verifyContact(id, code)`, `sendContactTest(id, idempotencyKey)` | member | Create an HTTP monitor checked from London and Frankfurt every minute: ```graphql mutation Create { createMonitor( idempotencyKey: "deploy-2026-10-22-homepage" input: { name: "Homepage" kind: HTTP spec: { url: "https://www.example.com/" } intervalS: 60 locations: { mode: LOCATIONS, codes: ["lon1", "fra1"] } labels: ["prod"] } ) { id version warnings locations { resolved } } } ``` `spec` is the check configuration of the kind, the same JSON as the REST `spec` (see the monitor types in the REST reference). In an update, `spec` is merged into the stored one: send only the members you change; arrays such as `headers` are replaced whole. Pause a monitor during maintenance and resume it afterwards: ```graphql mutation Pause($id: ID!) { pauseMonitor(id: $id) { runState } } mutation Resume($id: ID!) { resumeMonitor(id: $id) { runState state { status } } } ``` Acknowledge an incident and leave a note: ```graphql mutation Ack($id: ID!) { acknowledgeIncident(id: $id) { acknowledgedAt acknowledgedBy { name } } addIncidentNote(id: $id, body: "Rolling back the deploy") { id ts } } ``` Mutations in one request run in order, top to bottom, and later fields see the changes of earlier ones. ## Errors A field that fails is `null` and has an entry in `errors`. When the failure comes from the REST operation, its problem is in `extensions`: ```json { "data": null, "errors": [{ "message": "1 field is invalid.", "path": ["createMonitor"], "extensions": { "code": "validation", "status": 422, "requestId": "0192a3c4-…", "errors": [{ "pointer": "/interval_s", "code": "below_plan_minimum", "message": "Your plan allows intervals of 60 s or more." }] } }] } ``` Switch on `extensions.code`; the codes are the REST problem codes: | `code` | Meaning | |---|---| | `forbidden` | The token's role is too low, or the action needs a signed-in session (owner-only and password-confirmed actions never work with a token) | | `not_found` | Unknown id, or another organisation's | | `validation` | Input errors; `extensions.errors` has one entry per field, with a JSON Pointer into the REST body (`/interval_s`, `/spec/url`) | | `plan_limit` | Would exceed the plan; `limit`, `current`, `max` say which | | `precondition_failed` | `ifMatch` is stale | | `conflict`, `incident_resolved`, `contact_unverified`, ... | State conflicts, as in REST | | `rate_limited` | Slow down and retry | | `bad_request` | Missing `orgId`/`id` with a session, or a malformed argument | Errors in the query itself (a typo in a field name, a wrong argument type, a query over the limits below) have no `code`, only a `message` and `locations`. ## Limits - One operation per request, at most 64 KiB, answered within 30 s. - Depth at most 10 levels; complexity at most 10,000. Each field counts one, and a list's selection counts once per item it may return (`first`, default 50), so nested lists multiply: `monitors(first: 50)` selecting 10 fields costs about 500, and the same list inside each of 50 contacts about 25,000, which is refused (`extensions.code` is absent; the message says the query is too complex). Ask for smaller pages or split the query. - Each object or list a query loads runs one REST read, and counts against your rate limits like that REST request (600 reads a minute per token; writes 120 a minute per user and per organisation). Identical reads in one request run once. - Introspection is on; most GraphQL tools (GraphiQL, Insomnia, Postman, `graphql-codegen`) can load the schema from the endpoint with your token, or from the public `schema.graphql`. --- # MCP server JustMonitorIt runs a [Model Context Protocol](https://modelcontextprotocol.io) server, so AI assistants such as Claude, Cursor and VS Code agents can read your monitors, incidents and contacts, and change them if you allow it. Ask "is anything down?", "why did the API monitor alert last night?" or "add an HTTP check for https://example.com from Europe and North America". | Setting | Value | |---|---| | URL | `https://dash.justmonitorit.com/api/v1/mcp` | | Transport | Streamable HTTP (JSON responses, no SSE stream) | | Protocol | MCP `2026-07-28` (stateless). Clients on `2025-11-25`, `2025-06-18` or `2025-03-26` also work: the server answers `initialize` without starting a session. | | Authentication | An API token in the `Authorization: Bearer jmi_…` header on every request | | Capabilities | Tools (see the [tool reference](generated/mcp-tools.md)) | The server keeps no state between requests and never issues an `Mcp-Session-Id`, so you can reconnect, retry or run several agents with one token. ## 1. Create a token The MCP server uses the same API tokens as the REST and GraphQL APIs. In the web app, open **Settings → API tokens**, fill in **Create a token**: - **Name**: something you will recognise in the audit log, such as "Claude Code". - **Role**: what the assistant may do. - `viewer`: read monitors, results, incidents, contacts and the notification log. Choose this for assistants that only answer questions. - `member`: also create, change, pause and delete monitors, acknowledge incidents, add notes and choose which contacts a monitor alerts. - `admin`: also list members and read the audit log. A token can never do more than you can in that organisation, and it stops working if you leave the organisation. - **Expiry**: 90 days is a good default. Copy the token (`jmi_…`) when it is shown: it is not shown again. The token works on that one organisation; create one per organisation if you have several. ## 2. Connect your client Each client stores the URL and the `Authorization` header. Keep the token out of files you commit: most clients can read it from an environment variable, as below. ### Claude Code ```sh export JMI_TOKEN=jmi_… claude mcp add --transport http justmonitorit \ https://dash.justmonitorit.com/api/v1/mcp \ --header "Authorization: Bearer $JMI_TOKEN" ``` Add `--scope user` to use it in every project. Check it with `claude mcp list`, or `/mcp` inside Claude Code. ### Claude Desktop and claude.ai Custom connectors in Claude Desktop and on claude.ai sign in with OAuth, which JustMonitorIt does not offer yet. Connect Claude Desktop through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), a small local bridge that adds the header. In **Settings → Developer → Edit Config** (`claude_desktop_config.json`): ```json { "mcpServers": { "justmonitorit": { "command": "npx", "args": [ "-y", "mcp-remote", "https://dash.justmonitorit.com/api/v1/mcp", "--header", "Authorization:Bearer ${JMI_TOKEN}" ], "env": { "JMI_TOKEN": "jmi_…" } } } } ``` Restart Claude Desktop after saving. ### Cursor `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): ```json { "mcpServers": { "justmonitorit": { "url": "https://dash.justmonitorit.com/api/v1/mcp", "headers": { "Authorization": "Bearer ${env:JMI_TOKEN}" } } } } ``` ### VS Code `.vscode/mcp.json`; VS Code asks for the token once and stores it securely: ```json { "inputs": [ { "type": "promptString", "id": "jmi-token", "description": "JustMonitorIt API token", "password": true } ], "servers": { "justmonitorit": { "type": "http", "url": "https://dash.justmonitorit.com/api/v1/mcp", "headers": { "Authorization": "Bearer ${input:jmi-token}" } } } } ``` ### Other clients Any client that speaks Streamable HTTP and can send a custom header works. The usual JSON shape is: ```json { "justmonitorit": { "type": "http", "url": "https://dash.justmonitorit.com/api/v1/mcp", "headers": { "Authorization": "Bearer jmi_…" } } } ``` Client configuration formats change between versions; if yours differs, look for "remote", "HTTP" or "streamable HTTP" servers in its documentation. ### MCP Inspector To try the tools by hand: ```sh npx @modelcontextprotocol/inspector ``` Choose the **Streamable HTTP** transport, enter the URL, and add the header `Authorization` with the value `Bearer jmi_…`. ## 3. Tools `tools/list` returns only the tools your token's role can use. The [tool reference](generated/mcp-tools.md) lists every tool with its arguments. | Area | Tools | |---|---| | Your token and organisation | `whoami`, `get_overview`, `list_locations` | | Monitors | `list_monitors`, `get_monitor`, `create_monitor`, `update_monitor`, `pause_monitor`, `resume_monitor`, `delete_monitor`, `preview_check`, `get_monitor_results`, `get_monitor_uptime`, `set_monitor_contacts` | | Incidents | `list_incidents`, `get_incident`, `acknowledge_incident`, `add_incident_note` | | Contacts and alerts | `list_contacts`, `list_notifications` | | Administration | `list_members`, `get_audit_log` | Each tool calls the [REST API](generated/rest-reference.md) with your token, so it follows the same rules: the same validation, plan limits, rate limits and audit log. Results are the REST API's JSON, as `structuredContent` and as text. When the API refuses a call (a validation error, a plan limit, a missing permission), the tool result has `isError: true` and the API's [problem document](rest.md#errors), so the assistant can see what to fix. Every tool carries annotations: read tools are `readOnlyHint: true`, `delete_monitor` is `destructiveHint: true`. Most clients ask before running a tool that changes something; keep that confirmation on for `member` and `admin` tokens. ## Example prompts - "Which monitors are down or degraded right now, and since when?" - "Show me the incidents of the last 7 days for the checkout API and how long each lasted." - "What did the probes in Frankfurt see when the homepage went down?" (uses `get_monitor_results` with a location and time range) - "Was anyone notified about incident inc_…? Did the SMS arrive?" - "Check https://status.example.com from London once, then create an HTTP monitor for it every 60 seconds from Europe and North America." - "Acknowledge the open incident on the API monitor and add a note that we're rolling back the deploy." - "Pause every monitor labelled `staging`." - "Who changed the alert contacts of the billing monitor last week?" (needs an `admin` token) ## Security - **Least privilege.** Give assistants that only answer questions a `viewer` token. Use `member` only where the assistant should change things, and keep tool confirmations on. - **One token per client.** Then the audit log shows which tool made each change (`data.api_token`), and you can revoke one client without the others. - **Expiry and revocation.** Set an expiry, and revoke a token in **Settings → API tokens** as soon as you no longer need it or it may have leaked. Revocation takes effect on the next request. - **Prompt injection.** Monitor names, incident notes and check results can contain text written by other people or returned by the sites you check. Treat what an assistant proposes to change with the same care as any automated change. - The server accepts tokens only in the `Authorization` header (never in the URL), and refuses browser requests from other sites (`Origin` must be `https://dash.justmonitorit.com`). ## Protocol details For client authors; most users can skip this. - Every message is its own `POST` to the URL above; send `Accept: application/json, text/event-stream`. Responses are always `application/json`. A notification gets `202 Accepted` with no body. Batches (JSON arrays) are refused with `-32600`. - **Modern requests** (`2026-07-28`) carry `MCP-Protocol-Version: 2026-07-28`, `Mcp-Method` and, for `tools/call`, `Mcp-Name` (plain or `=?base64?…?=`), and `params._meta` with `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`. Mismatched or missing headers are `400` with `-32020`; missing `_meta` fields `400` with `-32602`; an unsupported version `400` with `-32022` and `data.supported`; an unknown method `404` with `-32601`. Results carry `resultType: "complete"` and `_meta["io.modelcontextprotocol/serverInfo"]`. `server/discover` returns the supported versions, capabilities and instructions. - `tools/list` results carry `ttlMs: 300000` and `cacheScope: "private"` (the list depends on the token's role). - **Legacy clients** send `initialize` first; the reply names the version (yours if supported, else `2025-11-25`) and has no `Mcp-Session-Id`. Later requests need no session id (one sent is ignored); `ping` answers `{}`. - `GET` and `DELETE` on the endpoint answer `405`: there is no standalone event stream and no session to end. - Authentication failures are HTTP `401` with `WWW-Authenticate: Bearer realm="JustMonitorIt", error="invalid_token"`. Each MCP request and each REST call a tool makes count against the token's [rate limits](rest.md#rate-limits). A raw request, for testing with `curl`: ```sh curl -s https://dash.justmonitorit.com/api/v1/mcp \ -H "Authorization: Bearer $JMI_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: list_monitors" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_monitors", "arguments": {"status": "down"}, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}}}}' ``` --- # MCP tools Every tool works on the organisation of the API token in use. `tools/list` returns only the tools the token's effective role can use. | Tool | Least role | Kind | What it does | |---|---|---|---| | [`whoami`](#whoami) | viewer | read | The API token in use: its name, role, effective role (what it may do: viewer reads; member also changes monitors, incidents and contacts; admin also reads members and the audit log) and the organisation it works on. | | [`get_overview`](#get_overview) | viewer | read | Counts of monitors by status (up, down, degraded, no_data, unknown, paused), the open incidents (newest first, up to 20), the latest status changes and plan usage. | | [`list_locations`](#list_locations) | viewer | read | The locations monitors are checked from: `code` (e.g. `lon1`), name, region (`eu`, `na`, ...) and whether it is available. | | [`list_monitors`](#list_monitors) | viewer | read | The organisation's monitors with their current status per location and 24 h uptime, filtered and paged. | | [`get_monitor`](#get_monitor) | viewer | read | One monitor: its spec, interval, locations, thresholds, contacts, current status per location, open incident and `version` (pass it as `if_match` when updating). | | [`create_monitor`](#create_monitor) | member | write | Create a monitor. | | [`update_monitor`](#update_monitor) | member | write | Change a monitor: give `monitor_id` and only the fields to change (JSON merge patch: `spec` is merged field by field, arrays are replaced whole, `null` clears a nullable field). | | [`pause_monitor`](#pause_monitor) | member | write | Stop checking a monitor (and alerting for it) until it is resumed. | | [`resume_monitor`](#resume_monitor) | member | write | Start checking a paused monitor again (subject to the plan's limits). | | [`delete_monitor`](#delete_monitor) | member | write, destructive | Delete a monitor with its history, incidents and pending notifications. | | [`preview_check`](#preview_check) | member | read | Run a check once, now, from one location, and return its result (outcome, error kind, timings) without saving anything or alerting. | | [`get_monitor_results`](#get_monitor_results) | viewer | read | A monitor's individual check results, newest first: per location, outcome (up, degraded, down, error = the probe's own problem), error kind and detail, HTTP status, latency and timings. | | [`get_monitor_pings`](#get_monitor_pings) | viewer | read | A heartbeat monitor's ping history, newest first (the newest 100 are kept): when each ping arrived, success or fail (the job reported a failure), the HTTP method, the client's address and user agent. | | [`get_monitor_uptime`](#get_monitor_uptime) | viewer | read | A monitor's uptime ratio and seconds down, degraded, without data and paused, for the windows 24h, 7d, 30d and 90d, or for one `from`-`to` range. | | [`set_monitor_contacts`](#set_monitor_contacts) | member | write | Replace the set of contacts a monitor alerts (contact ids `con_…` from list_contacts; an empty list alerts nobody). | | [`list_incidents`](#list_incidents) | viewer | read | Incidents (a monitor down or degraded, confirmed from several locations), newest first: status open or resolved, severity, cause, duration, acknowledgement. | | [`get_incident`](#get_incident) | viewer | read | One incident with its timeline (opened, status changes, acknowledgement, notes, resolution), the root cause per location and the notifications sent. | | [`acknowledge_incident`](#acknowledge_incident) | member | write | Acknowledge an open incident: reminders stop and the team sees someone is on it. | | [`add_incident_note`](#add_incident_note) | member | write | Add a note (1-2000 characters) to an incident's timeline. | | [`list_contacts`](#list_contacts) | viewer | read | Where alerts go: email addresses, SMS and phone call numbers, Pushover, Discord, PagerDuty, Slack and Microsoft Teams destinations and webhooks, with their status (unverified, active, disabled) and the alerts they get (down, recovered, degraded). | | [`list_notifications`](#list_notifications) | viewer | read | Alerts sent or attempted, newest first, with each delivery attempt and the provider's status. | | [`list_members`](#list_members) | admin | read | The organisation's members with their roles (owner, admin, member, viewer) and when they were last active. | | [`get_audit_log`](#get_audit_log) | admin | read | Who changed what in the organisation, newest first: actor, action (e.g. `monitor.updated`), target and changed fields. | ## `whoami` The API token in use: its name, role, effective role (what it may do: viewer reads; member also changes monitors, incidents and contacts; admin also reads members and the audit log) and the organisation it works on. Every other tool acts on that organisation. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. Arguments: none. ## `get_overview` Counts of monitors by status (up, down, degraded, no_data, unknown, paused), the open incidents (newest first, up to 20), the latest status changes and plan usage. Start here to answer "is anything down?". Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. Arguments: none. ## `list_locations` The locations monitors are checked from: `code` (e.g. `lon1`), name, region (`eu`, `na`, ...) and whether it is available. Use the codes and regions in a monitor's `locations` and in `preview_check`. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. Arguments: none. ## `list_monitors` The organisation's monitors with their current status per location and 24 h uptime, filtered and paged. Pass `next_cursor` back as `cursor` for the next page. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `cursor` | string | no | Opaque `next_cursor` of the previous page. | | `dir` | string | no | One of: asc, desc. | | `kind` | string | no | Comma-separated monitor kinds: icmp, http, http_adv, tcp, ssl_expiry, domain_expiry, imap, smtp, heartbeat. | | `label` | string | no | One label. | | `limit` | integer | no | Page size, default 50. | | `q` | string | no | Substring of the name or target. | | `run_state` | string | no | Comma-separated: active, paused, system_paused. | | `sort` | string | no | One of: created_at, name, status, status_since. | | `status` | string | no | Comma-separated: unknown, up, down, degraded, no_data, paused. | ## `get_monitor` One monitor: its spec, interval, locations, thresholds, contacts, current status per location, open incident and `version` (pass it as `if_match` when updating). Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `monitor_id` | string | yes | Monitor id (`mon_…`). | ## `create_monitor` Create a monitor. `kind` is one of icmp, http, http_adv, tcp, ssl_expiry, domain_expiry, imap, smtp, heartbeat, and `spec` holds that kind's fields: icmp {host}; tcp {host, port}; http {url, keyword?}; http_adv {url, method?, headers?, body?, expected_status? e.g. ["200-299"], assertions? e.g. [{"type": "body_contains", "value": "ok"}]}; ssl_expiry {host, port?, cert_expiry_warn_days?}; domain_expiry {domain, expiry_warn_days?} (interval_s must be 86400; omit `locations` and the thresholds); imap/smtp {host, port, tls: implicit|starttls|none}; heartbeat {grace_s?} (interval_s is the period between pings, 60-604800; omit `locations` and the thresholds; the response's ping_url is what the job requests). `interval_s` is 30, 60, 120, 300, 600, 900, 1800 or 3600 (at least the plan minimum). `locations` is {"mode": "regions", "regions": ["eu", "na"]} or {"mode": "locations", "codes": ["lon1", "fra1"]} (at least 2 locations; codes from list_locations). Omitting `contact_ids` attaches the organisation's auto-attach contacts. Test a target first with preview_check. Validation problems come back as an error naming each field. Least role: `member`. Annotations: readOnly false, destructive false, idempotent false. | Argument | Type | Required | Notes | |---|---|---|---| | `contact_ids` | array \| null | no | At most 100; omitted = every `auto_attach` contact. | | `degraded_after_ms` | integer \| null | no | `http_adv` only; 100-30000 and below the time budget; `null` = off. | | `failure_threshold` | integer \| null | no | 1-10, default 2; for `domain_expiry` 1, the default (a second probe confirms each change instead); for `heartbeat` 1. | | `idempotency_key` | string | no | Optional: repeat a call with the same key to get the first result instead of creating twice. | | `interval_s` | integer | yes | 30, 60, 120, 300, 600, 900, 1800 or 3600, at least the plan minimum. `domain_expiry`: 86400 only. `heartbeat`: the period between pings, 60-604800 (no plan minimum). | | `kind` | MonitorKind | yes | Immutable after create. | | `labels` | array | no | At most 10, each 1-32 characters of `[a-z0-9-_.]`. | | `locations` | see schema | no | | | `name` | string | yes | 1-100 characters. | | `paused` | boolean | no | | | `recovery_threshold` | integer \| null | no | 1-10, default 2; for `domain_expiry` and `heartbeat` 1, the default. | | `reminder_interval_s` | integer \| null | no | `null` = no reminders; 900-86400. | | `spec` | MonitorSpecInput | yes | The per-kind spec without the type tag (probes-and-checks.md §5): the `MonitorSpecInput` schema of `kind`; secret fields are write-only. | ## `update_monitor` Change a monitor: give `monitor_id` and only the fields to change (JSON merge patch: `spec` is merged field by field, arrays are replaced whole, `null` clears a nullable field). `kind` cannot change. `paused: true|false` pauses or resumes. Pass the `version` you read as `if_match` to avoid overwriting someone else's change. Least role: `member`. Annotations: readOnly false, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `contact_ids` | array \| null | no | | | `degraded_after_ms` | integer \| null | no | | | `failure_threshold` | integer \| null | no | | | `if_match` | integer | no | The monitor's `version` you last read; the call fails with `precondition_failed` if it changed since. | | `interval_s` | integer \| null | no | | | `kind` | see schema | no | | | `labels` | array \| null | no | | | `locations` | see schema | no | | | `monitor_id` | string | yes | Monitor id (`mon_…`). | | `name` | string \| null | no | | | `paused` | boolean \| null | no | `false` runs the same checks as `POST .../resume`. | | `recovery_threshold` | integer \| null | no | | | `reminder_interval_s` | integer \| null | no | | | `spec` | object \| null | no | Merged into the stored spec (RFC 7396): any subset of the `MonitorSpecInput` schema of the monitor's kind. | ## `pause_monitor` Stop checking a monitor (and alerting for it) until it is resumed. An open incident is resolved as paused. Least role: `member`. Annotations: readOnly false, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `monitor_id` | string | yes | Monitor id (`mon_…`). | ## `resume_monitor` Start checking a paused monitor again (subject to the plan's limits). Least role: `member`. Annotations: readOnly false, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `monitor_id` | string | yes | Monitor id (`mon_…`). | ## `delete_monitor` Delete a monitor with its history, incidents and pending notifications. This cannot be undone; confirm with the user first. Least role: `member`. Annotations: readOnly false, destructive true, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `if_match` | integer | no | The monitor's `version` you last read; the call fails with `precondition_failed` if it changed since. | | `monitor_id` | string | yes | Monitor id (`mon_…`). | ## `preview_check` Run a check once, now, from one location, and return its result (outcome, error kind, timings) without saving anything or alerting. Takes `kind` and `spec` as for create_monitor and a `location` code. Use it to test a target before creating a monitor. Can take up to about 45 seconds. Least role: `member`. Annotations: readOnly true, destructive false, idempotent false. | Argument | Type | Required | Notes | |---|---|---|---| | `kind` | MonitorKind | yes | | | `location` | string | yes | Location code. | | `spec` | MonitorSpecInput | yes | The spec as in `MonitorInput` (secrets inline). | ## `get_monitor_results` A monitor's individual check results, newest first: per location, outcome (up, degraded, down, error = the probe's own problem), error kind and detail, HTTP status, latency and timings. Default window: the last 24 hours; at most 7 days per call. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `cursor` | string | no | Opaque `next_cursor` of the previous page. | | `from` | string | no | RFC 3339 timestamp. | | `limit` | integer | no | Page size, default 50. | | `location` | string | no | A location code. | | `monitor_id` | string | yes | Monitor id (`mon_…`). | | `outcome` | string | no | Comma-separated: up, degraded, down, error. | | `to` | string | no | RFC 3339 timestamp. | ## `get_monitor_pings` A heartbeat monitor's ping history, newest first (the newest 100 are kept): when each ping arrived, success or fail (the job reported a failure), the HTTP method, the client's address and user agent. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `limit` | integer | no | How many, default 50. | | `monitor_id` | string | yes | Monitor id (`mon_…`). | ## `get_monitor_uptime` A monitor's uptime ratio and seconds down, degraded, without data and paused, for the windows 24h, 7d, 30d and 90d, or for one `from`-`to` range. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `from` | string | no | RFC 3339 timestamp. | | `monitor_id` | string | yes | Monitor id (`mon_…`). | | `to` | string | no | RFC 3339 timestamp. | | `windows` | string | no | Comma-separated of 24h, 7d, 30d, 90d (default all). | ## `set_monitor_contacts` Replace the set of contacts a monitor alerts (contact ids `con_…` from list_contacts; an empty list alerts nobody). Least role: `member`. Annotations: readOnly false, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `contact_ids` | array | yes | | | `monitor_id` | string | yes | Monitor id (`mon_…`). | ## `list_incidents` Incidents (a monitor down or degraded, confirmed from several locations), newest first: status open or resolved, severity, cause, duration, acknowledgement. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `cursor` | string | no | Opaque `next_cursor` of the previous page. | | `from` | string | no | RFC 3339 timestamp. | | `limit` | integer | no | Page size, default 50. | | `monitor_id` | string | no | Monitor id (`mon_…`). | | `severity` | string | no | One of: down, degraded. | | `status` | string | no | One of: open, resolved. | | `to` | string | no | RFC 3339 timestamp. | ## `get_incident` One incident with its timeline (opened, status changes, acknowledgement, notes, resolution), the root cause per location and the notifications sent. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `incident_id` | string | yes | Incident id (`inc_…`). | ## `acknowledge_incident` Acknowledge an open incident: reminders stop and the team sees someone is on it. Least role: `member`. Annotations: readOnly false, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `incident_id` | string | yes | Incident id (`inc_…`). | ## `add_incident_note` Add a note (1-2000 characters) to an incident's timeline. Least role: `member`. Annotations: readOnly false, destructive false, idempotent false. | Argument | Type | Required | Notes | |---|---|---|---| | `body` | string | yes | | | `incident_id` | string | yes | Incident id (`inc_…`). | ## `list_contacts` Where alerts go: email addresses, SMS and phone call numbers, Pushover, Discord, PagerDuty, Slack and Microsoft Teams destinations and webhooks, with their status (unverified, active, disabled) and the alerts they get (down, recovered, degraded). Secrets are never shown. Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `cursor` | string | no | Opaque `next_cursor` of the previous page. | | `kind` | string | no | One of: email, sms, voice, pushover, discord, pagerduty, slack, webhook, teams. | | `limit` | integer | no | Page size, default 50. | | `status` | string | no | One of: unverified, active, disabled. | ## `list_notifications` Alerts sent or attempted, newest first, with each delivery attempt and the provider's status. Filter by contact, incident or monitor to answer "was anyone told?". Least role: `viewer`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `contact_id` | string | no | | | `cursor` | string | no | Opaque `next_cursor` of the previous page. | | `incident_id` | string | no | Incident id (`inc_…`). | | `kind` | string | no | down, recovered, degraded, flapping, reminder, test, ... | | `limit` | integer | no | Page size, default 50. | | `monitor_id` | string | no | Monitor id (`mon_…`). | | `status` | string | no | One of: pending, sending, sent, failed, cancelled, suppressed. | ## `list_members` The organisation's members with their roles (owner, admin, member, viewer) and when they were last active. Least role: `admin`. Annotations: readOnly true, destructive false, idempotent true. Arguments: none. ## `get_audit_log` Who changed what in the organisation, newest first: actor, action (e.g. `monitor.updated`), target and changed fields. Entries made with an API token name it in `data.api_token`. Least role: `admin`. Annotations: readOnly true, destructive false, idempotent true. | Argument | Type | Required | Notes | |---|---|---|---| | `action` | string | no | Exact action, e.g. monitor.updated. | | `actor` | string | no | | | `cursor` | string | no | Opaque `next_cursor` of the previous page. | | `limit` | integer | no | Page size, default 50. | --- ```graphql """ An API token (never its secret). """ type ApiToken { id: ID! name: String! role: TokenRole! """ The first 8 characters after `jmi_`. """ prefix: String! createdBy: UserRef! createdAt: DateTime! expiresAt: DateTime lastUsedAt: DateTime lastUsedIp: String } """ Who made an audit entry. """ type AuditActor { kind: AuditActorKind! id: ID label: String } """ Who made an audit entry. """ enum AuditActorKind { USER PLATFORM_ADMIN SYSTEM STRIPE } """ One audit log entry. """ type AuditEntry { id: ID! ts: DateTime! actor: AuditActor! """ `area.verb`, e.g. `monitor.updated`. """ action: String! target: AuditTarget ip: String """ Changed fields and non-secret values; `api_token` when a token made it. """ data: JSON! } """ A page of a list. """ type AuditEntryConnection { nodes: [AuditEntry!]! pageInfo: PageInfo! } """ What an audit entry is about. """ type AuditTarget { type: String! id: String } """ Status of the subscription. """ enum BillingStatus { TRIALING ACTIVE PAST_DUE CANCELED } """ A notification channel. """ enum ChannelKind { EMAIL SMS PUSHOVER DISCORD PAGERDUTY SLACK WEBHOOK VOICE TEAMS } """ One check result. """ type CheckResult { ts: DateTime! location: String! """ The location whose slot a substitute ran. """ slotLocation: String workKind: WorkKind! outcome: Outcome! errorKind: ErrorKind errorDetail: String statusCode: Int latencyMs: Int timings: CheckTimings! tlsNotAfter: DateTime resolvedIp: String metrics: JSON } """ A page of a list. """ type CheckResultConnection { nodes: [CheckResult!]! pageInfo: PageInfo! } """ Phase timings of a check. """ type CheckTimings { dnsMs: Int connectMs: Int tlsMs: Int ttfbMs: Int totalMs: Int } """ Somewhere notifications go: an email address, a phone (SMS or calls), a Pushover user, a Discord webhook, a PagerDuty service, a Slack channel, a Microsoft Teams channel or a customer webhook. """ type Contact { id: ID! name: String! kind: ChannelKind! """ Kind-specific settings; secrets are never returned (`secretSet`). """ config: JSON! """ Voice only: the SMS credits each call to this number uses. """ creditsPerCall: Int secretSet: Boolean! notifyOn: [NotifyOn!]! """ Added to new monitors automatically. """ autoAttach: Boolean! status: ContactStatus! disabledReason: ContactDisabledReason verifiedAt: DateTime lastDeliveryAt: DateTime lastError: String consecutiveFailures: Int! monitorCount: Int! """ Pass as `ifMatch` to updates. """ version: Int! createdAt: DateTime! updatedAt: DateTime! """ Monitors that notify this contact. """ monitors(first: Int, after: String): MonitorConnection! """ Notifications to this contact, newest first. """ notifications(first: Int, after: String): NotificationConnection! } """ A page of a list. """ type ContactConnection { nodes: [Contact!]! pageInfo: PageInfo! } """ Why a contact is disabled. """ enum ContactDisabledReason { USER BOUNCED COMPLAINT OPTED_OUT INVALID UNSUBSCRIBED FAILING ABUSE } """ A new contact (REST `ContactInput`). """ input ContactInput { name: String! kind: ChannelKind! """ Email `{"address"}`, SMS and voice `{"phone"}`, Pushover `{"device", ...}`, Discord `{}`, PagerDuty `{"region"}`, Slack `{}`, webhook `{}`, Teams `{}`. """ config: JSON! """ Write-only: Pushover `{"user_key"}`, Discord `{"webhook_url"}`, PagerDuty `{"routing_key"}`, Slack `{"webhook_url"}`, webhook `{"url"}`, Teams `{"webhook_url"}`. """ secret: JSON """ Default `[DOWN, RECOVERED]`. """ notifyOn: [NotifyOn!] autoAttach: Boolean } """ Changes to a contact (REST `ContactPatch`); the kind cannot change. """ input ContactPatch { name: String """ A new email address or phone number sends a new verification code. """ config: JSON """ Omitted keeps the stored secret. """ secret: JSON notifyOn: [NotifyOn!] autoAttach: Boolean } """ `{ id, name, kind }` of a contact. """ type ContactRef { id: ID! name: String! kind: ChannelKind! } """ Status of a contact. """ enum ContactStatus { UNVERIFIED ACTIVE DISABLED } """ An RFC 3339 UTC timestamp with milliseconds, e.g. `2026-10-06T12:00:00.000Z`. """ scalar DateTime """ One delivery attempt of a notification. """ type Delivery { id: ID! attempt: Int! status: DeliveryStatus! httpStatus: Int errorCode: String error: String providerStatus: ProviderStatus providerStatusAt: DateTime startedAt: DateTime! latencyMs: Int! } """ Status of one delivery attempt. """ enum DeliveryStatus { SENT FAILED_RETRYABLE FAILED_PERMANENT AMBIGUOUS } """ Why a check failed. """ enum ErrorKind { DNS_FAILURE BLOCKED_ADDRESS CONNECT_TIMEOUT CONNECTION_REFUSED CONNECTION_RESET HOST_UNREACHABLE TLS_ERROR CERT_EXPIRED TIMEOUT PROTOCOL_ERROR STATUS_MISMATCH ASSERTION_FAILED TOO_MANY_REDIRECTS REDIRECT_BLOCKED PACKET_LOSS SLOW_RESPONSE CERT_EXPIRING DOMAIN_EXPIRING DOMAIN_EXPIRED DOMAIN_NOT_FOUND RDAP_UNAVAILABLE HEARTBEAT_MISSED HEARTBEAT_FAILED NETWORK_UNREACHABLE ICMP_UNAVAILABLE RESOURCE_EXHAUSTED PROBE_OVERLOADED INTERNAL } """ One ping a heartbeat monitor received. """ type HeartbeatPing { ts: DateTime! kind: PingKind! """ `GET`, `HEAD` or `POST`. """ method: String! remoteIp: String userAgent: String } """ An outage or degradation of a monitor, from first failure to recovery. """ type Incident { id: ID! severity: IncidentSeverity! status: IncidentStatus! currentStatus: IncidentCurrentStatus! flapping: Boolean! firstFailureAt: DateTime! openedAt: DateTime! resolvedAt: DateTime resolution: Resolution durationS: Int! acknowledgedAt: DateTime """ `null` with `acknowledgedAt` set: acknowledged through Pushover. """ acknowledgedBy: UserRef cause: String! causeDetail: JSON! remindersSent: Int! notificationCount: Int! """ The monitor's id and name. """ monitorRef: MonitorRef! """ The monitor. """ monitor: Monitor! """ The incident's timeline. """ events: [IncidentEvent!]! """ Notifications sent about the incident. """ notifications: [Notification!]! } """ A page of a list. """ type IncidentConnection { nodes: [Incident!]! pageInfo: PageInfo! } """ Current status of an incident's monitor. """ enum IncidentCurrentStatus { UP DOWN DEGRADED NO_DATA } """ One event of an incident. """ type IncidentEvent { id: ID! ts: DateTime! type: IncidentEventType! actor: UserRef data: JSON! } """ Type of an incident event. """ enum IncidentEventType { CHECK_FAILED OPENED STATUS_CHANGED LOCATION_FAILED LOCATION_RECOVERED NO_DATA DATA_RESUMED FLAPPING_STARTED FLAPPING_ENDED ACKNOWLEDGED REMINDER_SENT REMINDERS_EXHAUSTED NOTE RESOLVED NO_CONTACTS NOTIFICATION_SENT NOTIFICATION_FAILED NOTIFICATION_SUPPRESSED NOTIFICATION_UNDELIVERED } """ Worst status an incident reached. """ enum IncidentSeverity { DOWN DEGRADED } """ Whether an incident is open. """ enum IncidentStatus { OPEN RESOLVED } """ A scalar that can represent any JSON value. """ scalar JSON """ A check location. """ type Location { id: ID! """ Short code, e.g. `lon1`; monitors name locations by code. """ code: String! name: String! region: Region! countryCode: String! ipv6: Boolean! """ Active and with at least one eligible probe. """ available: Boolean! } """ How a monitor's locations are chosen. """ enum LocationMode { REGIONS LOCATIONS } """ Active locations and their regions. """ type Locations { regions: [RegionInfo!]! locations: [Location!]! } """ A member of an organisation. """ type Member { user: MemberUser! role: Role! joinedAt: DateTime! lastActiveAt: DateTime } """ The user of a membership. """ type MemberUser { id: ID! name: String! email: String! totpEnabled: Boolean! } """ A monitor: what is checked, from where, and its live state. """ type Monitor { id: ID! name: String! kind: MonitorKind! """ Check settings for the kind; secret values are never returned. """ spec: JSON! intervalS: Int! """ Execution budget derived from the spec. """ timeoutMs: Int! """ `null` for `DOMAIN_EXPIRY`: each lookup runs from one location, chosen each day; and for `HEARTBEAT`, which runs no checks. """ locations: MonitorLocations """ `HEARTBEAT` only: the URL its job requests each time it runs. """ pingUrl: String failureThreshold: Int! recoveryThreshold: Int! degradedAfterMs: Int reminderIntervalS: Int labels: [String!]! contactIds: [ID!]! runState: RunState! systemPauseReason: SystemPauseReason state: MonitorState! """ Uptime over the last 24 hours; `null` when nothing was observed. """ uptime24h: Float """ Create and update responses only: `does_not_resolve`, `two_locations`. """ warnings: [String!]! """ Pass as `ifMatch` to updates. """ version: Int! createdAt: DateTime! updatedAt: DateTime! """ Check results, newest first (default the last 24 h, at most 7 days per request). `first` 1-1000 (default 50). """ results( from: DateTime, to: DateTime, """ Location code """ location: String, outcome: Outcome, first: Int, after: String ): CheckResultConnection! """ `HEARTBEAT` only: the newest pings, newest first (the newest 100 are kept). `first` 1-100 (default 50). """ pings(first: Int): [HeartbeatPing!]! """ Uptime over standard windows (default all four), or over `from`..`to`. """ uptime(windows: [TimeWindow!], from: DateTime, to: DateTime): [Uptime!]! """ Status periods between `from` and `to`. """ timeline(from: DateTime, to: DateTime): [StatusPeriod!]! """ Response times per location over a window. """ series(range: TimeWindow! = LAST_24H): Series! """ This monitor's incidents, newest first. """ incidents(first: Int, after: String): IncidentConnection! """ The open incident, if any. """ openIncident: Incident """ Contacts notified about this monitor. """ contacts: [Contact!]! } """ A page of a list. """ type MonitorConnection { nodes: [Monitor!]! pageInfo: PageInfo! } """ A new monitor (REST `MonitorInput`). """ input MonitorInput { name: String! kind: MonitorKind! """ Check settings for the kind, e.g. `{"url": "https://example.com/"}`. """ spec: JSON! """ 30, 60, 120, 300, 600, 900, 1800 or 3600, at least the plan minimum. `DOMAIN_EXPIRY`: 86400 only. `HEARTBEAT`: the period between pings, 60-604800 (no plan minimum). """ intervalS: Int! """ Required, except for `DOMAIN_EXPIRY` and `HEARTBEAT`, which must omit it. """ locations: MonitorLocationsInput """ 1-10, default 2; `DOMAIN_EXPIRY` and `HEARTBEAT` 1 only. """ failureThreshold: Int """ 1-10, default 2; `DOMAIN_EXPIRY` and `HEARTBEAT` 1 only. """ recoveryThreshold: Int """ `HTTP_ADV` only; `null` = off. """ degradedAfterMs: Int """ 900-86400; `null` = no reminders. """ reminderIntervalS: Int labels: [String!] """ Omitted: the contacts marked auto-attach. """ contactIds: [ID!] paused: Boolean } """ Check type of a monitor. """ enum MonitorKind { ICMP HTTP HTTP_ADV TCP SSL_EXPIRY DOMAIN_EXPIRY IMAP SMTP HEARTBEAT } """ State of one location of a monitor. """ type MonitorLocationState { code: String! status: SlotStatus! """ Code of the location standing in for this one, if any. """ substitute: String lastResultAt: DateTime latencyMs: Int errorKind: ErrorKind } """ A monitor's locations. """ type MonitorLocations { mode: LocationMode! regions: [Region!] codes: [String!] """ Location codes the monitor runs from now. """ resolved: [String!]! } """ Where a monitor checks from. """ input MonitorLocationsInput { mode: LocationMode! """ With `mode: REGIONS`. """ regions: [Region!] """ Location codes, with `mode: LOCATIONS`. """ codes: [String!] } """ Changes to a monitor (REST `MonitorPatch`); the kind cannot change. """ input MonitorPatch { name: String """ Merged into the stored spec (JSON Merge Patch): arrays are replaced whole; `null` members clear. """ spec: JSON intervalS: Int locations: MonitorLocationsInput failureThreshold: Int recoveryThreshold: Int degradedAfterMs: Int reminderIntervalS: Int labels: [String!] contactIds: [ID!] """ `false` runs the same checks as `resumeMonitor`. """ paused: Boolean } """ `{ id, name }` of a monitor. """ type MonitorRef { id: ID! name: String! } """ Sort of the monitor list. """ enum MonitorSort { CREATED_AT NAME STATUS STATUS_SINCE } """ Live state of a monitor. """ type MonitorState { status: MonitorStatus! statusSince: DateTime! lastResultAt: DateTime openIncidentId: ID locations: [MonitorLocationState!]! """ `HEARTBEAT` only: when the newest ping arrived. """ lastPingAt: DateTime } """ Status of a monitor. """ enum MonitorStatus { UNKNOWN UP DOWN DEGRADED NO_DATA PAUSED } type Mutation { """ Create a monitor (member role). `idempotencyKey` (8-64 characters `[A-Za-z0-9_-]`) makes a retry return the first result. """ createMonitor(orgId: ID, input: MonitorInput!, idempotencyKey: String): Monitor! """ Change a monitor (member role). `ifMatch` is the `version` you read; a stale one fails with `precondition_failed`. """ updateMonitor(orgId: ID, id: ID!, patch: MonitorPatch!, ifMatch: Int): Monitor! """ Delete a monitor and its history (member role). Returns its id. """ deleteMonitor(orgId: ID, id: ID!, ifMatch: Int): ID! """ Pause a monitor (member role): no checks, no alerts. """ pauseMonitor(orgId: ID, id: ID!): Monitor! """ Resume a paused monitor (member role); checks plan limits. """ resumeMonitor(orgId: ID, id: ID!): Monitor! """ Replace the contacts a monitor notifies (member role). """ setMonitorContacts(orgId: ID, id: ID!, contactIds: [ID!]!): Monitor! """ Acknowledge an open incident (member role): stops reminders. """ acknowledgeIncident(orgId: ID, id: ID!): Incident! """ Add a note (1-2000 characters) to an incident's timeline (member role). """ addIncidentNote(orgId: ID, id: ID!, body: String!): IncidentEvent! """ Create a contact (member role). Email, SMS, voice and Teams contacts start unverified and are sent a code (see `verifyContact`). """ createContact(orgId: ID, input: ContactInput!, idempotencyKey: String): Contact! """ Change a contact (member role). """ updateContact(orgId: ID, id: ID!, patch: ContactPatch!, ifMatch: Int): Contact! """ Delete a contact (member role). Returns its id. """ deleteContact(orgId: ID, id: ID!, ifMatch: Int): ID! """ Re-enable a disabled contact (member role). """ enableContact(orgId: ID, id: ID!): Contact! """ Stop notifying a contact (member role). """ disableContact(orgId: ID, id: ID!): Contact! """ Send (again) the verification code of an email, SMS, voice or Teams contact, or re-check a Pushover, Discord, PagerDuty, Slack or webhook one (member role). """ sendContactVerification(orgId: ID, id: ID!): Boolean! """ Verify an email, SMS, voice or Teams contact with the code it received (member role). """ verifyContact(orgId: ID, id: ID!, code: String!): Contact! """ Send a test notification to a contact (member role). Returns the notification's id; follow it with `organization { notification(id) }`. """ sendContactTest(orgId: ID, id: ID!, idempotencyKey: String): ID! """ Replace the monitors that notify a contact (member role). """ setContactMonitors(orgId: ID, id: ID!, monitorIds: [ID!]!): Contact! } """ One notification to one contact. """ type Notification { id: ID! kind: NotificationKind! channel: ChannelKind! status: NotificationStatus! statusReason: NotificationStatusReason contact: ContactRef monitorId: ID incidentId: ID attempts: Int! nextAttemptAt: DateTime createdAt: DateTime! finishedAt: DateTime lastError: String deliveries: [Delivery!]! } """ A page of a list. """ type NotificationConnection { nodes: [Notification!]! pageInfo: PageInfo! } """ Kind of a notification. """ enum NotificationKind { DOWN RECOVERED DEGRADED FLAPPING REMINDER TEST FALLBACK ACCOUNT_NOTICE PUSHOVER_CANCEL } """ Status of a notification. """ enum NotificationStatus { PENDING SENDING SENT FAILED CANCELLED SUPPRESSED HELD } """ Why a notification has its status. """ enum NotificationStatusReason { SMS_CAP ANOMALY_BRAKE NOTIFY_HOLD CONTACT_DISABLED MONITOR_DELETED MONITOR_PAUSED PERMANENT_FAILURE GAVE_UP MAINTENANCE } """ Which changes notify a contact. """ enum NotifyOn { DOWN RECOVERED DEGRADED } """ Plan and subscription status of an organisation. """ type OrgBilling { plan: Plan! status: BillingStatus! } """ `{ id, name }` of an organisation. """ type OrgRef { id: ID! name: String! } """ Status of an organisation. """ enum OrgStatus { ACTIVE SUSPENDED DELETING } """ An organisation and everything in it. """ type Organization { id: ID! name: String! timezone: String! status: OrgStatus! """ Set while the organisation is scheduled for deletion. """ purgeAfter: DateTime """ The caller's role (an API token's effective role). """ myRole: Role! billing: OrgBilling! createdAt: DateTime! """ Monitor counts, open incidents and recent changes. """ overview: Overview! """ Monitors, filtered and sorted. `first` 1-500 (default 50). """ monitors( status: [MonitorStatus!], kind: [MonitorKind!], label: String, runState: [RunState!], contactId: ID, """ Substring of the name or target """ search: String, sort: MonitorSort, direction: SortDirection, first: Int, after: String ): MonitorConnection! """ One monitor. """ monitor(id: ID!): Monitor! """ Incidents, newest first. `first` 1-200 (default 50). """ incidents(status: IncidentStatus, monitorId: ID, severity: IncidentSeverity, from: DateTime, to: DateTime, first: Int, after: String): IncidentConnection! """ One incident, with its events and notifications. """ incident(id: ID!): Incident! """ Contacts. `first` 1-200 (default 50). """ contacts(kind: ChannelKind, status: ContactStatus, first: Int, after: String): ContactConnection! """ One contact. """ contact(id: ID!): Contact! """ The notification log, newest first. `first` 1-200 (default 50). """ notifications(contactId: ID, incidentId: ID, monitorId: ID, status: NotificationStatus, kind: NotificationKind, first: Int, after: String): NotificationConnection! """ One notification and its delivery attempts. """ notification(id: ID!): Notification! """ Members and their roles. """ members: [Member!]! """ The audit log, newest first (admin role). `first` 1-200 (default 50). """ auditLog( """ Exact action, e.g. `monitor.updated` """ action: String, """ Actor user id """ actor: ID, first: Int, after: String ): AuditEntryConnection! """ Effective plan limits and current usage: `{entitlements, usage}`. """ entitlements: JSON! } """ Outcome of a check. """ enum Outcome { UP DEGRADED DOWN ERROR } """ The organisation's dashboard summary. """ type Overview { monitors: OverviewMonitors! """ Newest first, at most 20. """ openIncidents: [Incident!]! """ The last 20 status changes. """ recentChanges: [OverviewChange!]! usage: OverviewUsage! } """ A recent status change. """ type OverviewChange { monitor: MonitorRef! from: MonitorStatus to: MonitorStatus! at: DateTime! incidentId: ID } """ Counts of monitors by status. """ type OverviewMonitors { total: Int! up: Int! down: Int! degraded: Int! noData: Int! unknown: Int! paused: Int! } """ Plan usage on the overview. """ type OverviewUsage { monitorsUsed: Int! monitorsLimit: Int! smsCreditsUsed: Int! smsCreditsLimit: Int! smsPeriodEnd: DateTime! } """ Paging state of a connection. """ type PageInfo { """ Pass as `after` for the next page. """ endCursor: String hasNextPage: Boolean! } """ Whether a heartbeat's job reported success or failure. """ enum PingKind { SUCCESS FAIL } """ A plan. """ enum Plan { TRIAL STARTER PRO BUSINESS } """ What the provider reported later. """ enum ProviderStatus { DELIVERED UNDELIVERED FAILED BOUNCED COMPLAINT ACKNOWLEDGED } type Query { """ The calling API token, its organisation and effective role; `null` when signed in with a session. """ token: TokenInfo """ An organisation. `id` defaults to an API token's own organisation; it is required with a session. """ organization(id: ID): Organization! """ Active check locations and their regions (public). """ locations: Locations! } """ A region of locations. """ enum Region { EU NA SA AS OC AF } """ A region. """ type RegionInfo { code: Region! name: String! } """ How an incident ended. """ enum Resolution { RECOVERED PAUSED } """ A member's role. """ enum Role { OWNER ADMIN MEMBER VIEWER } """ Whether the scheduler runs a monitor. """ enum RunState { ACTIVE PAUSED SYSTEM_PAUSED } """ The response-time chart of a monitor. """ type Series { range: String! stepS: Int! from: DateTime! to: DateTime! series: [SeriesLine!]! bands: [SeriesBand!]! } """ A non-up period in a series. """ type SeriesBand { from: DateTime! to: DateTime! status: MonitorStatus! } """ Response times of one location: `[unix seconds, latency ms or null]`. """ type SeriesLine { location: String! points: [[Int]!]! } """ Status of one location of a monitor. """ enum SlotStatus { UP DOWN DEGRADED NO_DATA } """ Sort direction. """ enum SortDirection { ASC DESC } """ A period with one status. """ type StatusPeriod { status: MonitorStatus! startedAt: DateTime! endedAt: DateTime incidentId: ID } """ Why the system paused a monitor. """ enum SystemPauseReason { BILLING PLAN_LIMIT ORG_SUSPENDED ORG_DELETING ADMIN } """ A standard time window (uptime windows, series ranges). """ enum TimeWindow { LAST_24H LAST_7D LAST_30D LAST_90D } """ The calling API token. """ type TokenInfo { token: ApiToken! org: OrgRef! """ `min(token role, creator's current role)`: what the token may do. """ effectiveRole: TokenRole! } """ An API token's role. """ enum TokenRole { ADMIN MEMBER VIEWER } """ Uptime over a window. """ type Uptime { """ `24h`, `7d`, `30d`, `90d`; `null` for an explicit `from`/`to`. """ window: String from: DateTime! to: DateTime! """ `null` when nothing was observed. """ uptimeRatio: Float observedS: Int! downS: Int! degradedS: Int! noDataS: Int! pausedS: Int! unknownS: Int! } """ `{ id, name }` of a user. """ type UserRef { id: ID! name: String! } """ Why a check ran. """ enum WorkKind { ROUND RECHECK SHADOW CANARY PREVIEW TRACE } """ Directs the executor to include this field or fragment only when the `if` argument is true. """ directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ Directs the executor to skip this field or fragment when the `if` argument is true. """ directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT schema { query: Query mutation: Mutation } ```