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 - Request body:
{"query": "...", "variables": {...}, "operationName": "..."}asContent-Type: application/json - Response:
200with{"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 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:
{ "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
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 } } } }"}'
{
"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):
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:
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_sisintervalS,status_sinceisstatusSince. - Enums are the REST values in capitals:
no_dataisNO_DATA,http_advisHTTP_ADV. - Ids are the REST ids (
mon_…,inc_…,con_…), of typeID. DateTimeis an RFC 3339 UTC string with milliseconds,2026-10-06T12:00:00.000Z.JSONholds free-form objects exactly as REST returns them: a monitor'sspec, an incident'scauseDetail, an audit entry'sdata,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.
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.
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:
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:
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:
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:
{
"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.codeis 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 publicschema.graphql.