# 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](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](authentication.md) 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:

```json
{ "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

```bash
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 } } } }"}'
```

```json
{
  "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):

```js
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`:

```python
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:

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

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

```graphql
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:

```graphql
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:

```graphql
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:

```graphql
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`:

```json
{
  "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.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`.
