imsu

View this page as Markdown

REST API

#

The REST API lets scripts, CI pipelines and agents manage the monitors of an imsu.io account with an API key.

  • Base URL: https://imsu.io/api/v1
  • Authentication: Authorization: Bearer imsu_pat_… (API keys)
  • Bodies: JSON, at most 64 KB, sent with Content-Type: application/json
  • OpenAPI 3.1 document: https://imsu.io/api/v1/openapi.json
  • Available on every plan; the plan's limits apply as in the web app.

AI clients that speak MCP can use the MCP server instead, which signs in with OAuth.

API keys

#

Create a key

#
  1. Sign in at https://imsu.io/settings and find API keys.
  2. Give the key a name, e.g. "GitHub Actions".
  3. Choose its access: Read and write, or Read only.
  4. Choose when it expires: never, or after 30 days, 90 days or 1 year.
  5. Click Create key and copy the key. It is shown this once; imsu.io stores only its hash.

A key acts as the user who created it and can reach every monitor of the account. An account can have 50 keys that are not revoked (expired keys count until they are revoked), and a user can create 20 keys an hour. Only people can create keys: there is no API for it.

Send the key

#

Send the key in the Authorization header of every request. Keys in the query string are ignored.

curl https://imsu.io/api/v1/monitors -H "Authorization: Bearer $IMSU_API_KEY"

The same key also works for the MCP server at https://imsu.io/mcp. OAuth access tokens (imsu_at_…) don't work here, and neither does the web app's sign-in cookie.

Scopes

#
Access Scopes Allows
Read only monitors:read GET requests: the key, the plan's limits, monitors
Read and write monitors:read monitors:write Everything, including creating, changing, pausing, resuming and deleting monitors

A read-only key that tries to change something gets 403 insufficient-scope.

Expiry and revoking

#

A key stops working when it expires, when it is revoked in Settings → API keys, when its user leaves the account, or while the account is suspended. Each of these takes effect on the next request, which gets 401 unauthorized.

Conventions

#
  • JSON with camelCase names; enums are lowercase strings ("http", "unknown").
  • Times are RFC 3339 in UTC, e.g. 2026-11-10T19:10:46.865908+00:00. Ids are UUIDs.
  • Errors are RFC 9457 problem details (application/problem+json) whose type links to its section below.
  • Every response has an X-Request-Id header, and every problem has the same value as requestId. Send your own X-Request-Id (1 to 64 letters, digits, ., _ or -) to have it used instead, so your logs and imsu.io's share it.
  • CORS: any origin may call the API, without cookies. Allowed request headers: Authorization, Content-Type, Idempotency-Key, X-Request-Id. Readable response headers: Location, X-Request-Id, Retry-After, Idempotent-Replayed.
  • Versioning: /api/v1 only gets additions (new fields, operations and optional parameters). Breaking changes go to /api/v2, and v1 operations being retired send Deprecation and Sunset headers for at least 6 months first. Ignore fields you don't know.

Monitors

#

A monitor is one of three types, fixed when it is created:

  • http: requests url and checks the status code and, optionally, that the body contains keyword.
  • port: opens a TCP connection to host on port. With expectClosed, it is up while the port refuses connections and down when it accepts them.
  • heartbeat: waits for a job to request pingUrl at least every intervalSeconds plus graceSeconds. The job requests it (GET, POST or HEAD) after each run, or pingUrl followed by /fail to report a failed run. A job can also report the progress of each run (heartbeat pings).

Fields you send

#
Field Types Default Rules
name all (required) At most 100 characters
type all http http, port or heartbeat; can't change later
url http (required) Absolute http:// or https:// URL, at most 2048 characters
host port (required) Domain name or IP address, without a scheme or path
port port (required) 1 to 65535
intervalSeconds all The plan's minimum Seconds between checks, from the plan's minimum (minIntervalSeconds in limits) to 86400 (1 day); for heartbeats the longest time expected between pings, up to 2678400 (31 days)
timeoutSeconds http, port 10 1 to 30, and shorter than the interval
method http GET GET or HEAD
expectedStatus http 200-299 Status codes that count as up, codes and ranges from 100 to 599, e.g. 200-299,301
keyword http none Text the body must contain, case-sensitive, at most 200 characters; not with HEAD
followRedirects http true Follow redirects and check the final response
failureThreshold http, port 2 Failed checks in a row before the monitor is down and alerts go out, 1 to 10
expectClosed port false Reverse check: alert when the port accepts connections
sslExpiryAlerts http true for an https:// URL, otherwise false Alert before the certificate expires (SSL certificate checks); true needs an https:// URL
sslAlertDays http 14 Days before expiry the alerts start: 1, 3, 7, 14 or 30
graceSeconds heartbeat 300 How late a ping may be, 0 to 86400
runTimeoutSeconds heartbeat 3600 How long a run that reports progress (heartbeat pings) may take, 60 to 86400

Fields of other types are ignored. Addresses that aren't reachable from the public internet can't be checked.

Fields you get back

#

Every operation that returns a monitor returns all of these:

Field
id The monitor's id
name, type As sent
target What is checked, for display: the URL, host:port, or heartbeat
url, host, port, method, intervalSeconds, timeoutSeconds, expectedStatus, keyword, followRedirects, failureThreshold, expectClosed, graceSeconds, runTimeoutSeconds, sslExpiryAlerts, sslAlertDays The settings in effect. Fields of other types are null or their default
isActive false while the monitor is paused
status unknown (not checked yet, or no ping yet), up or down: the last known state, also while paused
downReason ssl while status is down because of a certificate error, otherwise null
lastCheckedAt, lastResponseMs The last check, or null
nextCheckAt When the next check runs (for heartbeats: when the next ping is due)
createdAt, updatedAt
uptime24h Percentage of passed checks in the last 24 hours, or null without checks
heartbeatToken, pingUrl, lastPingAt Heartbeats: the secret part of the ping URL, the full URL the job requests, and when the last ping came
ssl The certificate last seen on an https:// URL: expiresAt, daysLeft (whole days, negative once expired), issuer, subject and checkedAt. null for other monitors and until a check has seen one

Heartbeat pings

#

A heartbeat's job requests these URLs with GET, POST or HEAD. They need no API key: the token in pingUrl is the secret. Each answers 200 with OK, 404 while the monitor is paused or deleted, or 400 with the reason when the run id or percent isn't valid.

URL Means
pingUrl The job is alive, or finished a run
pingUrl/fail The job failed: the monitor is down and alerts go out right away
pingUrl/eid-<run id>/<percent> Progress of one run, a whole number from 0 to 100. The first ping with a run id starts the run; 100 finishes it
pingUrl/eid-<run id>/fail The run failed: the monitor is down right away

A run id is 1 to 64 letters, digits, dots, dashes or underscores; use a new one for each run, such as the date and time. A run that hasn't reached 100 within runTimeoutSeconds of its first ping makes the monitor down. Every ping of a running run also counts as the job being alive. A job that only requests pingUrl and pingUrl/fail has no runs, so runTimeoutSeconds doesn't apply to it.

A nightly backup that reports its run:

RUN_ID=$(date +%Y%m%d-%H%M)
PING_URL=https://imsu.io/hb/aikP8KWzoY09jzPLr3N-7Q
curl -fsS -m 10 --retry 3 "$PING_URL/eid-$RUN_ID/0"
/path/to/backup.sh && curl -fsS -m 10 --retry 3 "$PING_URL/eid-$RUN_ID/100" || curl -fsS -m 10 --retry 3 "$PING_URL/eid-$RUN_ID/fail"

SSL certificate checks

#

HTTP monitors with an https:// URL also check the site's certificate, on the same request as the regular check:

  • SSL errors: an invalid certificate (expired, wrong host name, untrusted, incomplete chain) or a failed TLS handshake fails the check. After failureThreshold failed checks the monitor is down and alerts go out, as for any other failure. It shows as "SSL error" instead of "Down", with a cause such as "SSL certificate expired on Nov 3, 2026." In the API, status stays down and downReason is ssl; an incident's reason is ssl.
  • Expiry warnings: with sslExpiryAlerts on, an alert goes out when the certificate has 30, 14, 7, 3 and 1 days left, starting at sslAlertDays (14 by default), at most one per threshold. A renewed certificate starts them over. It is on by default for https:// URLs.
  • Certificate info: the issuer, subject, expiry date and days left are in ssl, on the monitor's page in the app, and in the MCP server's get_monitor and list_monitors.

Status pages show "SSL error" for such a monitor, but never the cause or the certificate.

Operations

#

All examples use $IMSU_API_KEY for the key.

Check the API key

#

GET /api/v1/me · any key

Returns the user the key acts as, the account, and the key's name, scopes and expiry.

curl https://imsu.io/api/v1/me -H "Authorization: Bearer $IMSU_API_KEY"
{
  "user": {
    "id": "01a1c6de-b461-7461-919d-3d918e658f67",
    "email": "[email protected]",
    "displayName": "Ada"
  },
  "accountId": "01a1c6de-b48f-783c-9a6e-3e17ba91dbdc",
  "credential": {
    "kind": "api_key",
    "id": "01a1c6de-b511-7f9d-900e-99d409cc4d1d",
    "name": "Deploy script",
    "scopes": ["monitors:read", "monitors:write"],
    "expiresAt": null
  }
}

Get the plan's limits

#

GET /api/v1/limits · monitors:read

curl https://imsu.io/api/v1/limits -H "Authorization: Bearer $IMSU_API_KEY"
{
  "maxMonitors": 10,
  "minIntervalSeconds": 300,
  "monitorsInUse": 3
}

maxMonitors counts paused monitors too. These are the free plan's numbers; imsu.io staff can set others per account.

List monitors

#

GET /api/v1/monitors · monitors:read

Query parameter
status up, down, unknown or paused
type http, port or heartbeat
q Text to find in the name or the target, ignoring case
limit Monitors per page, 1 to 500; default 100
cursor The nextCursor of the previous page (pagination)
sslExpiringWithinDays Only monitors whose SSL certificate expires within this many days, or has expired, e.g. 30 (SSL certificate checks)

Filters combine. Monitors come oldest first. counts counts every monitor of the account, whatever the filters; a paused monitor counts only as paused.

curl "https://imsu.io/api/v1/monitors?type=http&q=shop" -H "Authorization: Bearer $IMSU_API_KEY"
{
  "items": [
    {
      "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
      "name": "Shop",
      "type": "http",
      "target": "https://shop.example.com/health",
      "url": "https://shop.example.com/health",
      "host": null,
      "port": null,
      "method": "GET",
      "intervalSeconds": 300,
      "timeoutSeconds": 10,
      "expectedStatus": "200-299",
      "keyword": "ok",
      "followRedirects": true,
      "failureThreshold": 2,
      "isActive": true,
      "status": "up",
      "lastCheckedAt": "2026-11-10T19:35:47.112408+00:00",
      "lastResponseMs": 182,
      "nextCheckAt": "2026-11-10T19:40:47.112408+00:00",
      "createdAt": "2026-11-10T19:10:46.865908+00:00",
      "updatedAt": "2026-11-10T19:10:46.865908+00:00",
      "uptime24h": 100,
      "heartbeatToken": null,
      "graceSeconds": null,
      "runTimeoutSeconds": null,
      "lastPingAt": null,
      "expectClosed": false,
      "pingUrl": null
    }
  ],
  "counts": {
    "total": 3,
    "up": 3,
    "down": 0,
    "unknown": 0,
    "paused": 0
  },
  "nextCursor": null
}

Create a monitor

#

POST /api/v1/monitors · monitors:write

Send the fields; left-out fields get their defaults. The first check runs within a minute. Send an Idempotency-Key so a retry can't create a second monitor (idempotency): make a new key for each monitor, and send the same $KEY again when you retry this request.

KEY=$(uuidgen)
curl https://imsu.io/api/v1/monitors \
  -H "Authorization: Bearer $IMSU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"name":"Shop","url":"https://shop.example.com/health","intervalSeconds":300,"keyword":"ok"}'
HTTP/1.1 201 Created
Location: /api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745
X-Request-Id: 0HNPVBP9D77O3
Content-Type: application/json; charset=utf-8
{
  "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
  "name": "Shop",
  "type": "http",
  "target": "https://shop.example.com/health",
  "url": "https://shop.example.com/health",
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 300,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": "ok",
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": true,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-10T19:10:46.865908+00:00",
  "createdAt": "2026-11-10T19:10:46.865908+00:00",
  "updatedAt": "2026-11-10T19:10:46.865908+00:00",
  "uptime24h": null,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": null
}

A port monitor that alerts when a database port is reachable:

curl https://imsu.io/api/v1/monitors \
  -H "Authorization: Bearer $IMSU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"port","name":"Database","host":"db.example.com","port":5432,"expectClosed":true}'
{
  "id": "01a1c6de-b6c2-76f8-8888-bc82bb2f1703",
  "name": "Database",
  "type": "port",
  "target": "db.example.com:5432",
  "url": null,
  "host": "db.example.com",
  "port": 5432,
  "method": "GET",
  "intervalSeconds": 300,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": null,
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": true,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-10T19:10:46.979298+00:00",
  "createdAt": "2026-11-10T19:10:46.979298+00:00",
  "updatedAt": "2026-11-10T19:10:46.979298+00:00",
  "uptime24h": null,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": true,
  "pingUrl": null
}

A heartbeat for a nightly job, which may be up to 10 minutes late:

curl https://imsu.io/api/v1/monitors \
  -H "Authorization: Bearer $IMSU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"heartbeat","name":"Nightly backup","intervalSeconds":86400,"graceSeconds":600}'
{
  "id": "01a1c6de-b6d9-7f75-9453-fffe95be1731",
  "name": "Nightly backup",
  "type": "heartbeat",
  "target": "heartbeat",
  "url": null,
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 86400,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": null,
  "followRedirects": true,
  "failureThreshold": 1,
  "isActive": true,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-11T19:20:47.001537+00:00",
  "createdAt": "2026-11-10T19:10:47.001537+00:00",
  "updatedAt": "2026-11-10T19:10:47.001537+00:00",
  "uptime24h": null,
  "heartbeatToken": "aikP8KWzoY09jzPLr3N-7Q",
  "graceSeconds": 600,
  "runTimeoutSeconds": 3600,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": "https://imsu.io/hb/aikP8KWzoY09jzPLr3N-7Q"
}

The job then requests the ping URL after each run:

0 3 * * * /path/to/backup.sh && curl -fsS -m 10 --retry 3 https://imsu.io/hb/aikP8KWzoY09jzPLr3N-7Q > /dev/null

Get a monitor

#

GET /api/v1/monitors/{id} · monitors:read

curl https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745 -H "Authorization: Bearer $IMSU_API_KEY"
{
  "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
  "name": "Shop",
  "type": "http",
  "target": "https://shop.example.com/health",
  "url": "https://shop.example.com/health",
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 300,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": "ok",
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": true,
  "status": "up",
  "lastCheckedAt": "2026-11-10T19:35:47.112408+00:00",
  "lastResponseMs": 182,
  "nextCheckAt": "2026-11-10T19:40:47.112408+00:00",
  "createdAt": "2026-11-10T19:10:46.865908+00:00",
  "updatedAt": "2026-11-10T19:10:46.865908+00:00",
  "uptime24h": 100,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": null
}

A monitor of another account, or a deleted one, is 404 not-found.

Change some settings

#

PATCH /api/v1/monitors/{id} · monitors:write

Send only the fields to change; the others keep their values. "keyword": null removes the keyword (null on any other field keeps it). Sending a different type is a validation error.

curl -X PATCH https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745 \
  -H "Authorization: Bearer $IMSU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Shop health","keyword":null}'
{
  "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
  "name": "Shop health",
  "type": "http",
  "target": "https://shop.example.com/health",
  "url": "https://shop.example.com/health",
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 300,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": null,
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": true,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-10T19:10:47.149652+00:00",
  "createdAt": "2026-11-10T19:10:46.865908+00:00",
  "updatedAt": "2026-11-10T19:10:47.149652+00:00",
  "uptime24h": null,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": null
}

Replace all settings

#

PUT /api/v1/monitors/{id} · monitors:write

A full update, as the web app's form saves it: fields left out go back to their defaults. Send name and the type's required fields. The type can't change, so type may be left out. To change only some fields, use PATCH.

curl -X PUT https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745 \
  -H "Authorization: Bearer $IMSU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Shop","url":"https://shop.example.com/health","intervalSeconds":600}'
{
  "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
  "name": "Shop",
  "type": "http",
  "target": "https://shop.example.com/health",
  "url": "https://shop.example.com/health",
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 600,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": null,
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": true,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-10T19:10:47.149652+00:00",
  "createdAt": "2026-11-10T19:10:46.865908+00:00",
  "updatedAt": "2026-11-10T19:10:47.185344+00:00",
  "uptime24h": null,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": null
}

Pause a monitor

#

POST /api/v1/monitors/{id}/pause · monitors:write

Stops checks and alerts until the monitor is resumed. Settings and history are kept, and a paused monitor still counts toward the plan's limit. Pausing a paused monitor changes nothing. No body is needed.

curl -X POST https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745/pause -H "Authorization: Bearer $IMSU_API_KEY"
{
  "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
  "name": "Shop",
  "type": "http",
  "target": "https://shop.example.com/health",
  "url": "https://shop.example.com/health",
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 600,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": null,
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": false,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-10T19:10:47.149652+00:00",
  "createdAt": "2026-11-10T19:10:46.865908+00:00",
  "updatedAt": "2026-11-10T19:10:47.210486+00:00",
  "uptime24h": null,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": null
}

Resume a monitor

#

POST /api/v1/monitors/{id}/resume · monitors:write

Restarts checks. An http or port monitor is checked right away; a heartbeat gets its interval plus grace for the next ping. Resuming an active monitor changes nothing.

curl -X POST https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745/resume -H "Authorization: Bearer $IMSU_API_KEY"
{
  "id": "01a1c6de-b61a-74a3-b8a8-8ec96fb2d745",
  "name": "Shop",
  "type": "http",
  "target": "https://shop.example.com/health",
  "url": "https://shop.example.com/health",
  "host": null,
  "port": null,
  "method": "GET",
  "intervalSeconds": 600,
  "timeoutSeconds": 10,
  "expectedStatus": "200-299",
  "keyword": null,
  "followRedirects": true,
  "failureThreshold": 2,
  "isActive": true,
  "status": "unknown",
  "lastCheckedAt": null,
  "lastResponseMs": null,
  "nextCheckAt": "2026-11-10T19:10:47.248055+00:00",
  "createdAt": "2026-11-10T19:10:46.865908+00:00",
  "updatedAt": "2026-11-10T19:10:47.248055+00:00",
  "uptime24h": null,
  "heartbeatToken": null,
  "graceSeconds": null,
  "runTimeoutSeconds": null,
  "lastPingAt": null,
  "expectClosed": false,
  "pingUrl": null
}

Delete a monitor

#

DELETE /api/v1/monitors/{id} · monitors:write

Checks and alerts stop, and the monitor's history is no longer shown. This can't be undone; to stop checks for a while, pause instead.

curl -X DELETE https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745 -H "Authorization: Bearer $IMSU_API_KEY"
HTTP/1.1 204 No Content
X-Request-Id: 0HNPVBP9D77OP

Deleting it again is 404 not-found.

Pagination

#

GET /api/v1/monitors returns up to limit monitors (default 100, at most 500), oldest first. When nextCursor is not null, more follow: pass it as cursor, with the same filters, for the next page. Treat it as opaque. Pages stay stable while monitors are renamed or added.

curl "https://imsu.io/api/v1/monitors?limit=2" -H "Authorization: Bearer $IMSU_API_KEY"
curl "https://imsu.io/api/v1/monitors?limit=2&cursor=AaHG3rbCdviIiLyCuy8XAw" -H "Authorization: Bearer $IMSU_API_KEY"

The first answer ends with:

{
  "counts": {
    "total": 3,
    "up": 0,
    "down": 0,
    "unknown": 3,
    "paused": 0
  },
  "nextCursor": "AaHG3rbCdviIiLyCuy8XAw"
}

A cursor that wasn't returned by the API is a 400 validation-failed naming cursor.

Idempotency

#

POST /api/v1/monitors takes an Idempotency-Key header: any value of 1 to 255 printable ASCII characters, such as a UUID. Use a new key for each monitor you mean to create, and the same key when you retry the same request.

  • A retry with the same key and the same body within 24 hours gets the first response again, errors included, with Idempotent-Replayed: true. No second monitor is created.
  • The same key with a different body is 422 idempotency-key-reused.
  • While the first request is still running, a retry is 409 idempotency-key-in-use. If the first request died (its key is over a minute old and still unfinished), the retry runs it.
  • A 5xx answer or a refusal for a missing scope isn't stored, so a retry runs again.
  • Keys belong to the API key that sent them; another API key can use the same value.

A replay:

HTTP/1.1 201 Created
Location: /api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745
Idempotent-Replayed: true
X-Request-Id: 0HNPVBP9D77O4
Content-Type: application/json; charset=utf-8

Rate limits

#

Each API key may make 120 reads (GET) and 30 writes (other methods) a minute. Over a limit, the answer is 429 rate-limited with Retry-After in seconds. The MCP server has its own budget. The plan's limits still cap how many monitors there can be.

Errors

#

Errors are problem details. type is a link to the error's section below; tell errors apart by it, not by title or detail, which are written for people and may change.

{
  "type": "https://imsu.io/docs/api#not-found",
  "title": "Monitor not found",
  "status": 404,
  "detail": "There is no monitor with this id in the account. List the monitors with GET /api/v1/monitors to find ids.",
  "traceId": "00-85a40f77ea16afb81a64161f3d820443-f73d278f83970dd9-00",
  "requestId": "0HNPVBP9D77ON"
}

401 Unauthorized

#

No API key was sent, or it is unknown, revoked or expired, or its account is suspended. The WWW-Authenticate header is Bearer, with error="invalid_token" when a key was sent. OAuth access tokens get this too: they only work at /mcp.

{
  "type": "https://imsu.io/docs/api#unauthorized",
  "title": "API key required",
  "status": 401,
  "detail": "Create an API key in Settings → API keys and send it as Authorization: Bearer imsu_pat_….",
  "traceId": "00-f45be73064a3ae40197a9fa2e04924e5-695f651d33f494db-00",
  "requestId": "0HNPVBP9D77OQ"
}

With a key that doesn't work, the title is "Invalid API key"; with an OAuth token, "Token not accepted here". Get a working key from Settings → API keys.

403 Insufficient scope

#

A read-only key tried to change something. The answer also has WWW-Authenticate: Bearer error="insufficient_scope", scope="monitors:write".

{
  "type": "https://imsu.io/docs/api#insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This needs the monitors:write scope, which the API key was not given. Create a key with that scope in Settings → API keys.",
  "traceId": "00-992c3f8b9a2ac54443ad353bf7b239ba-8b45093bc87f891f-00",
  "requestId": "0HNPVBP9D77OS"
}

Use a key with read and write access.

404 Not found

#

There is no monitor with this id in the key's account: it never existed, was deleted, or belongs to another account (which looks the same). A path that doesn't exist, such as an id that isn't a UUID, gets this type too, titled "Not Found". List the monitors to find the right id.

400 Validation failed

#

Something in the request is wrong. errors names each field (by its JSON name), query parameter or header (Idempotency-Key), or body when the body isn't a JSON object.

{
  "type": "https://imsu.io/docs/api#validation-failed",
  "title": "Invalid request",
  "status": 400,
  "detail": "Correct the fields listed in errors and try again.",
  "errors": {
    "name": ["Name is required."],
    "url": ["URL must be an absolute http:// or https:// address."],
    "intervalSeconds": ["Interval must be between 300 and 86400 seconds on your plan."]
  },
  "traceId": "00-fd3db058be97209d1352532328302713-f75288ca44ef3510-00",
  "requestId": "0HNPVBP9D77OI"
}

A value of the wrong type is named the same way, e.g. "intervalSeconds": ["The value has the wrong type for this field."]. Fix each named field and send again.

403 Monitor limit reached

#

Every monitor the plan allows is in use; paused monitors count.

{
  "type": "https://imsu.io/docs/api#monitor-limit-reached",
  "title": "Monitor limit reached",
  "status": 403,
  "detail": "Your plan allows up to 10 monitors, and all are in use. Delete one you no longer need first.",
  "traceId": "00-b5e372e86465852a295db52462921e4c-ce5ec2d5053a4f05-00",
  "requestId": "0HNPVBP9D77P5"
}

Don't retry. Tell the user; they can delete a monitor they no longer need. GET /api/v1/limits shows the numbers.

409 Duplicate monitor

#

Reserved: /api/v1 doesn't return it today, because it creates a second monitor for the same target when asked. The MCP server's create_monitor refuses duplicates unless allow_duplicate is true, and tells the model which monitor exists (MCP server). If /api/v1 ever returns it, existingMonitorId names the monitor that already checks the target.

422 Idempotency key reused

#

The Idempotency-Key was used before, within 24 hours, for a request with a different body.

{
  "type": "https://imsu.io/docs/api#idempotency-key-reused",
  "title": "Idempotency key reused",
  "status": 422,
  "detail": "This idempotency key was used for a request with a different body. Use a new key for a new request.",
  "traceId": "00-8ada563899e6e0f290bf207d27708d2c-183e4abbf55070f3-00",
  "requestId": "0HNPVBP9D77O5"
}

Use a new key for a new request.

409 Idempotency key in use

#

The first request with this Idempotency-Key is still running.

{
  "type": "https://imsu.io/docs/api#idempotency-key-in-use",
  "title": "Idempotency key in use",
  "status": 409,
  "detail": "A request with this idempotency key is still running. Retry in a few seconds to get its response.",
  "traceId": "00-3c5e8f1a2b4d6e7f8a9b0c1d2e3f4a5b-6c7d8e9f0a1b2c3d-00",
  "requestId": "0HNPVBP9D78A2"
}

Wait a few seconds and retry with the same key and body.

429 Rate limited

#

The API key made more requests this minute than its limit. Retry-After says how many seconds to wait.

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-Request-Id: 0HNPVBP9D78B7
Content-Type: application/problem+json
{
  "type": "https://imsu.io/docs/api#rate-limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "This credential sent more requests than its limit allows. Wait 23 seconds (Retry-After) and try again.",
  "traceId": "00-7d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a-0b1c2d3e4f5a6b7c-00",
  "requestId": "0HNPVBP9D78B7"
}

413 Body too large

#

The body is over 64 KB. A monitor takes a few hundred bytes.

{
  "type": "https://imsu.io/docs/api#body-too-large",
  "title": "Request body too large",
  "status": 413,
  "detail": "The body can be at most 64 KB.",
  "traceId": "00-e46acf10d873806bb50bfa7448721005-c2d310bc953a3f71-00",
  "requestId": "0HNPVBP9D77OM"
}

415 Unsupported media type

#

POST, PUT and PATCH with a body need Content-Type: application/json.

{
  "type": "https://imsu.io/docs/api#unsupported-media-type",
  "title": "Unsupported Media Type",
  "status": 415,
  "traceId": "00-d081ae8b16ecacdefa6dd4ca5fede1e3-445b819e9feadc3d-00",
  "requestId": "0HNPVBP9D77OL"
}

Server errors

#

A 500 or 503 is a failure on imsu.io's side. Retry later; creates with an Idempotency-Key are safe to retry. If it persists, report the X-Request-Id.

OpenAPI document

#

The OpenAPI 3.1 description of /api/v1 is at https://imsu.io/api/v1/openapi.json. It needs no key and may be read from any origin. Every operation in it is described on this page.