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
#- Sign in at https://imsu.io/settings and find API keys.
- Give the key a name, e.g. "GitHub Actions".
- Choose its access: Read and write, or Read only.
- Choose when it expires: never, or after 30 days, 90 days or 1 year.
- 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) whosetypelinks to its section below. - Every response has an
X-Request-Idheader, and every problem has the same value asrequestId. Send your ownX-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/v1only gets additions (new fields, operations and optional parameters). Breaking changes go to/api/v2, and v1 operations being retired sendDeprecationandSunsetheaders 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: requestsurland checks the status code and, optionally, that the body containskeyword.port: opens a TCP connection tohostonport. WithexpectClosed, it is up while the port refuses connections and down when it accepts them.heartbeat: waits for a job to requestpingUrlat least everyintervalSecondsplusgraceSeconds. The job requests it (GET, POST or HEAD) after each run, orpingUrlfollowed by/failto 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
failureThresholdfailed 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,statusstaysdownanddownReasonisssl; an incident'sreasonisssl. - Expiry warnings: with
sslExpiryAlertson, an alert goes out when the certificate has 30, 14, 7, 3 and 1 days left, starting atsslAlertDays(14 by default), at most one per threshold. A renewed certificate starts them over. It is on by default forhttps://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'sget_monitorandlist_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
5xxanswer 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.