# 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.
