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
viewertoken keeps it read-only.