Examples | JustMonitorIt API
On this page

Examples

Complete recipes against the REST API. They assume a token in the environment variable JMI_TOKEN (Authentication) and use the token's own organisation, found with GET /token.

export JMI_TOKEN=jmi_...
export JMI_API=https://dash.justmonitorit.com/api/v1

The shell examples use jq, the Python ones requests and the JavaScript ones the fetch and crypto built into Node.js 20 and later.

Who am I?

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.

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

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)"'
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.

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:

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

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

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

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:

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:

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:

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.
  • To let an AI assistant read your monitors and incidents, or act on them, connect it to the MCP server with a token; a viewer token keeps it read-only.