GraphQL | JustMonitorIt API
On this page

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": "..."} 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 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_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:

FieldReturns
tokenThe calling token, its organisation and effective role (null for a session)
organization(id: ID)An organisation; id defaults to the token's
locationsActive check locations and regions (also without a token)

Everything else hangs off Organization:

FieldNotes
overviewMonitor 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
membersMembers and roles
auditLog(action, actor, first, after)Admin role
entitlementsPlan 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.

MutationRole
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:

codeMeaning
forbiddenThe 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_foundUnknown id, or another organisation's
validationInput errors; extensions.errors has one entry per field, with a JSON Pointer into the REST body (/interval_s, /spec/url)
plan_limitWould exceed the plan; limit, current, max say which
precondition_failedifMatch is stale
conflict, incident_resolved, contact_unverified, ...State conflicts, as in REST
rate_limitedSlow down and retry
bad_requestMissing 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.