# imsu.io

> imsu.io ("is my server up?") is an uptime monitoring service. It checks websites and HTTP APIs, TCP ports and cron jobs on a schedule, records whether each was up, how long it took and why it failed, and alerts the owner by email, Slack or Telegram when one goes down and when it comes back up. The free plan covers up to 10 monitors, each checked at most every 5 minutes, with no credit card.

## For agents

AI agents can manage a user's monitors (list, read, create, change, pause, resume, delete) through an MCP server at `https://imsu.io/mcp`, which signs in with OAuth, or through a REST API at `https://imsu.io/api/v1` with an API key the user creates. Only the user can sign up, sign in, approve access and create API keys, in the browser.

- [Start here: imsu.io for AI agents](https://imsu.io/docs/agents.md): which way to connect, what only the user can do, and what to do about each error
- [MCP server](https://imsu.io/docs/mcp.md): setup for Claude, Claude Code, Cursor, VS Code and other clients, and the tools
- [Authentication](https://imsu.io/docs/authentication.md): OAuth 2.1 for MCP clients, request by request
- [REST API](https://imsu.io/docs/api.md): API keys and every `/api/v1` operation with examples
- [Everything in one file](https://imsu.io/llms-full.txt): this file followed by all the docs pages

## Getting started

- [Home page](https://imsu.io/): what imsu.io does, how it works and the free plan
- [Create a free account](https://imsu.io/register): an email address and a password; no credit card
- [Sign in](https://imsu.io/login)
- [Privacy](https://imsu.io/docs/privacy.md): what personal data imsu.io keeps, including for status pages, the API and MCP, and how long

## What a monitor checks

- **HTTP:** any public `http://` or `https://` URL, requested with GET or HEAD. Up means a response within the timeout (1–30 seconds, 10 by default) with an expected status code (codes or ranges such as `200-299,301`; `200-299` by default) and, optionally, a keyword that must appear in the response body (GET only). Redirects are followed by default and can be turned off.
- **SSL certificates:** for `https://` URLs, an invalid certificate or failed TLS handshake marks the monitor down, labeled "SSL error". Alerts also go out before the certificate expires, at 30, 14, 7, 3 and 1 days left up to a chosen threshold (14 days by default; on by default for https). The issuer, subject and expiry date are shown per monitor and returned by the API and MCP ([SSL certificate checks](https://imsu.io/docs/api#ssl-certificate-checks)).
- **TCP port:** a connection to a host and port, e.g. a mail or database server. A reverse check alerts when a port that should stay closed accepts connections.
- **Heartbeat:** a cron job, backup or worker requests its own ping URL after each run (or the URL with `/fail` to report a failure). The monitor is down when no ping arrives within the expected interval plus a grace period. Intervals can be up to 31 days.
- Addresses that are not reachable from the public internet (private or local networks) cannot be monitored.
- Checks send the user agent `imsu.io-uptime-monitor/1.0` and come from the addresses listed at [https://imsu.io/api/meta/ips](https://imsu.io/api/meta/ips). [imsu.io bot](https://imsu.io/docs/bot.md) explains how to verify, allow or stop them.
- Interval: every 5 minutes at most on the free plan, up to once a day. Faster checks (down to every 10 seconds) can be enabled per account by imsu.io staff.
- No false alarms from a single dropped request: a failed check is retried within 30 seconds, and an HTTP or port monitor is marked down only after a set number of consecutive failures (1–10, 2 by default).
- Monitors can be paused and resumed.

## Alerts

- Sent when a monitor goes down (with the reason, e.g. a timeout, an unexpected status code or an SSL error) and when it is back up (with how long it was down), and before an https site's certificate expires.
- Channels: email (to the login address or another verified address), a Slack incoming webhook, and a Telegram bot. Each channel can be turned on and off separately.

## History and reports

- A dashboard with every monitor's current status, last response time and 24-hour uptime.
- Per monitor: individual checks with response times and errors, uptime and response time statistics, daily uptime bars, and every incident with its cause and duration.
- Individual check results are kept for 30 days; daily uptime figures for about 13 months.

## Plans

- Free: up to 10 monitors, checked every 5 minutes at most. No credit card. The MCP server and the REST API are included.
- Higher limits (more monitors, faster checks) are set per account by imsu.io staff; there is no self-service paid plan yet.

## Optional

- [AI catalog](https://imsu.io/.well-known/ai-catalog.json): machine-readable list of the resources imsu.io offers to AI agents
- [OpenAPI document](https://imsu.io/api/v1/openapi.json) of the REST API
- [Sitemap](https://imsu.io/sitemap.xml)

---

URL: https://imsu.io/docs/agents
Markdown: https://imsu.io/docs/agents.md

# imsu.io for AI agents

This page is for AI agents first and people second. It says what imsu.io does, how an agent connects to a user's account, what only the user can do, and what to do about each error.

Everything here is also Markdown: [https://imsu.io/docs/agents.md](https://imsu.io/docs/agents.md). All the docs in one file: [https://imsu.io/llms-full.txt](https://imsu.io/llms-full.txt).

## What imsu.io does

imsu.io checks a user's services on a schedule and alerts them by email, Slack or Telegram when one goes down and when it is back up.

- **http** monitors request a URL with GET or HEAD and check the status code and, optionally, a keyword in the body.
- **port** monitors open a TCP connection to `host:port`. A reverse check alerts when a port that should be closed (a database, say) accepts connections.
- **heartbeat** monitors wait for a job (cron, backup, worker) to request its ping URL. They are down when the pings stop.
- The free plan allows 10 monitors, each checked at most every 5 minutes (300 seconds). There is no credit card and no paid plan to buy yet.

An agent can list, read, create, change, pause, resume and delete the monitors of the user's account. It acts as the user, with the access the user gave it.

## Which way to connect

1. **You are an MCP client** (Claude, Claude Code, Cursor, VS Code, ChatGPT or another client that supports remote MCP servers with OAuth): connect to the MCP server at `https://imsu.io/mcp`. Your client starts OAuth when the server answers `401`; the user signs in to imsu.io in the browser and allows access. Setup per client: [MCP server](https://imsu.io/docs/mcp). The OAuth flow in detail: [Authentication](https://imsu.io/docs/authentication).
2. **You run in a shell, a script or CI, or your MCP client can't do OAuth**: ask the user for an API key. They create one at [https://imsu.io/settings](https://imsu.io/settings), under API keys. Send it as `Authorization: Bearer imsu_pat_…` to the REST API at `https://imsu.io/api/v1` ([REST API](https://imsu.io/docs/api)) or to the MCP server at `https://imsu.io/mcp`.

Check an API key before you use it:

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

```json
{
  "user": {
    "id": "01a1c6de-b461-7461-919d-3d918e658f67",
    "email": "you@example.com",
    "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
  }
}
```

A key whose `scopes` lack `monitors:write` can only read.

## What only the user can do

An agent can't do these. Ask the user to do them in the browser, then carry on.

- **Sign up.** The user creates an account at [https://imsu.io/register](https://imsu.io/register) with an email address and a password. There is no API for it.
- **Sign in.** Never ask for or type the user's password. Sign-in has a bot check, and OAuth exists so the agent never sees the password.
- **Approve access.** The consent page at `https://imsu.io/authorize` asks the user, not the agent. The user may allow reading only.
- **Create, see or revoke API keys.** Only in Settings. A key is shown once, when it is created.
- **Disconnect an app.** Settings → Connected apps, or the app revokes its own tokens.
- **Change alert channels, status pages, the account or the plan.** Only in the web app. Higher limits are set by imsu.io staff; there is no self-service upgrade.

Also not possible: monitoring addresses that aren't reachable from the public internet (private or local networks), and reading check history or incidents through the API (`get_monitor` shows the open incident and the last 24 hours' uptime).

## Good habits

- Find monitor ids with `list_monitors` or `GET /api/v1/monitors`. Never guess an id.
- Intervals are in seconds: 300 is 5 minutes. They can't be shorter than the account's minimum (`get_account_limits`, `GET /api/v1/limits`).
- Ask the user before deleting a monitor; say which one. Deleting can't be undone; pausing can.
- `create_monitor` refuses a second monitor for the same target (heartbeats: with the same name) unless you pass `allow_duplicate: true`. Pass it only when the user wants a second one.
- On the REST API, send an `Idempotency-Key` with each create, so a retry can't create two monitors.

## When you see X, do Y

`/api/v1` errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details whose `type` links to their section of the docs. `/mcp` answers HTTP errors the same way; problems inside a tool call come back as a tool result with `isError: true` and a sentence that says how to fix the call.

| You see | Where | It means | Do this |
|---|---|---|---|
| `401` with `resource_metadata` in `WWW-Authenticate` | `/mcp` | No token, or one that is unknown, expired or revoked | MCP client: start OAuth from the `resource_metadata` URL, or refresh the access token first if you have a refresh token. Headless: ask the user for an API key. [Authentication](https://imsu.io/docs/mcp#authentication) |
| `401` [unauthorized](https://imsu.io/docs/api#unauthorized) | `/api/v1` | No API key, or one that is unknown, revoked or expired, or the account is suspended; OAuth tokens are refused here too | Ask the user for a valid API key from Settings → API keys |
| `403` with `error="insufficient_scope"` | `/mcp` | The user allowed this app to read only, and the tool changes monitors | Tell the user, then run the authorization again asking for `monitors:read monitors:write`. [Insufficient scope](https://imsu.io/docs/mcp#insufficient-scope) |
| Tool error "This API key can only read monitors…" | `/mcp` | A read-only API key called a tool that changes monitors | Ask the user for a key with read and write access |
| `403` [insufficient-scope](https://imsu.io/docs/api#insufficient-scope) | `/api/v1` | A read-only key tried to change monitors | Ask the user for a key with read and write access |
| `403` [monitor-limit-reached](https://imsu.io/docs/api#monitor-limit-reached), or a tool error about the plan's limit | both | Every monitor the plan allows is in use (paused ones count) | Tell the user the limit. They can delete a monitor they no longer need. Don't retry |
| `400` [validation-failed](https://imsu.io/docs/api#validation-failed), or a tool error "Invalid arguments…" | both | A field, parameter or header is wrong; each one is named | Fix the named fields and send again |
| `404` [not-found](https://imsu.io/docs/api#not-found), or a tool error "No monitor with id…" | both | No such monitor in this account | List the monitors to find the right id |
| Tool error "Monitor … already checks …" or "A heartbeat named … already exists" | `/mcp` | A monitor for the same target exists (heartbeats: one with the same name) | Use that monitor. Pass `allow_duplicate: true` only if the user wants a second one |
| `409` [idempotency-key-in-use](https://imsu.io/docs/api#idempotency-key-in-use) | `/api/v1` | The first request with this `Idempotency-Key` is still running | Wait a few seconds and retry with the same key and body |
| `422` [idempotency-key-reused](https://imsu.io/docs/api#idempotency-key-reused) | `/api/v1` | The key was used before with a different body | Use a new key for a new request |
| `413` [body-too-large](https://imsu.io/docs/api#body-too-large) or [body too large](https://imsu.io/docs/mcp#body-too-large) | both | The body is over 64 KB (`/api/v1`) or 256 KB (`/mcp`) | Send a smaller body; a monitor takes a few hundred bytes |
| `415` [unsupported-media-type](https://imsu.io/docs/api#unsupported-media-type), or `415` "Unsupported charset" | both | The body isn't JSON, or not in a charset the server reads | Send JSON with `Content-Type: application/json` (UTF-8) |
| `429` [rate-limited](https://imsu.io/docs/api#rate-limited) or [too many requests](https://imsu.io/docs/mcp#rate-limits) | both | The credential sent too many requests this minute | Wait the number of seconds in `Retry-After`, then retry |
| `405` with `Allow: POST` | `/mcp` | `GET` and `DELETE` open and close streams, which this server doesn't use | Nothing: send messages with `POST` |
| `400` from `/mcp` with a JSON-RPC error | `/mcp` | A batch (a JSON array), or an unknown `MCP-Protocol-Version` | Send one message per request with a version the server named at `initialize` |
| `500` or `503` | both | Something failed on imsu.io's side | Retry later. On `/api/v1`, quote the `X-Request-Id` header if you report it |

## Where to go next

- [MCP server](https://imsu.io/docs/mcp): client setup, the tools, examples
- [Authentication](https://imsu.io/docs/authentication): OAuth 2.1 for MCP clients, request by request
- [REST API](https://imsu.io/docs/api): API keys and every `/api/v1` operation
- [OpenAPI document](https://imsu.io/api/v1/openapi.json) of the REST API

---

URL: https://imsu.io/docs/mcp
Markdown: https://imsu.io/docs/mcp.md

# MCP server

imsu.io runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server at:

```text
https://imsu.io/mcp
```

With it, an AI client can list, read, create, change, pause, resume and delete the monitors of your imsu.io account. You sign in to imsu.io in your browser once and choose what the client may do; the client never sees your password.

- Transport: Streamable HTTP, without sessions. Every message is a `POST` to `https://imsu.io/mcp`.
- Protocol versions: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05.
- Sign-in: OAuth 2.1 with PKCE, started by the client ([Authentication](https://imsu.io/docs/authentication)). Clients without OAuth can send an API key instead.
- Available on every plan. The plan's limits apply as in the web app.

The steps below come from each client's own documentation. Menus change; if one doesn't match, look for "custom connector" or "remote MCP server" in the client's help.

## Connect a client

### Claude (web, desktop and mobile)

1. On [claude.ai](https://claude.ai) or in Claude Desktop, open **Customize → Connectors**.
2. Click **+ Add**, then **Add custom connector**.
3. Name it `imsu.io` and enter the URL `https://imsu.io/mcp`. Click **Continue**.
4. Click **Continue** on the sign-in settings Claude detected, keep its defaults and click **Add**. Sign in to imsu.io in the browser window that opens and click **Allow**.
5. In a chat, open **+ → Connectors** and turn imsu.io on.

Connectors run from Anthropic's servers, so a connector you add on the web works in Claude Desktop and the mobile apps too. On Team and Enterprise plans an owner adds it under **Organization settings → Connectors**, and each member connects their own imsu.io account. Free Claude accounts can have one custom connector.

### Claude Code

```sh
claude mcp add --transport http imsu https://imsu.io/mcp
```

Then, in Claude Code, run `/mcp`, choose imsu, and sign in in the browser. To use the server in every project, add it with `--scope user`; to share it with your team through `.mcp.json`, with `--scope project`:

```sh
claude mcp add --transport http --scope user imsu https://imsu.io/mcp
```

#### Claude Code without a browser

For CI or a headless machine, create an API key in [Settings → API keys](https://imsu.io/settings) and pass it as a header:

```sh
claude mcp add --transport http imsu https://imsu.io/mcp \
  --header "Authorization: Bearer $IMSU_API_KEY"
```

Or in `.mcp.json`, with the key read from the environment:

```json
{
  "mcpServers": {
    "imsu": {
      "type": "http",
      "url": "https://imsu.io/mcp",
      "headers": {
        "Authorization": "Bearer ${IMSU_API_KEY}"
      }
    }
  }
}
```

### Cursor

Add the server to `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (one project):

```json
{
  "mcpServers": {
    "imsu": {
      "url": "https://imsu.io/mcp"
    }
  }
}
```

Cursor signs in with OAuth when the server asks for it; allow access in the browser. To use an API key instead, add `"headers": { "Authorization": "Bearer ${env:IMSU_API_KEY}" }`.

### VS Code

Run **MCP: Add Server** from the Command Palette and choose **HTTP**, or add the server to `.vscode/mcp.json` (or to your user configuration, **MCP: Open User Configuration**):

```json
{
  "servers": {
    "imsu": {
      "type": "http",
      "url": "https://imsu.io/mcp"
    }
  }
}
```

VS Code opens the browser to sign in the first time it connects. To use an API key instead, prompt for it so it isn't stored in the file:

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "imsu-api-key",
      "description": "imsu.io API key",
      "password": true
    }
  ],
  "servers": {
    "imsu": {
      "type": "http",
      "url": "https://imsu.io/mcp",
      "headers": {
        "Authorization": "Bearer ${input:imsu-api-key}"
      }
    }
  }
}
```

### MCP Inspector

```sh
npx @modelcontextprotocol/inspector --server-url https://imsu.io/mcp --transport http
```

Press **Connect**, then sign in and allow access in the browser. The Inspector registers itself as an OAuth client. With an API key, add `--header "Authorization: Bearer $IMSU_API_KEY"`.

### ChatGPT and other clients

Any client that supports remote MCP servers over Streamable HTTP with OAuth can connect: give it `https://imsu.io/mcp`. In ChatGPT, custom MCP connectors are added in developer mode (**Settings → Apps**); whether it is offered depends on your ChatGPT plan. Clients that only take a fixed header can send an API key as `Authorization: Bearer imsu_pat_…`.

## Authentication {#authentication}

Every request needs a credential in the `Authorization` header:

- an **OAuth access token** (`imsu_at_…`) that an MCP client got through the flow in [Authentication](https://imsu.io/docs/authentication), or
- an **API key** (`imsu_pat_…`) the user created in [Settings → API keys](https://imsu.io/settings).

Cookies never work here, and tokens in the query string are ignored. A request without a credential gets `401`, and the `WWW-Authenticate` header says where to start OAuth:

```http
POST /mcp HTTP/1.1
Host: imsu.io
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
```

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://imsu.io/.well-known/oauth-protected-resource/mcp", scope="monitors:read monitors:write"
Content-Type: application/problem+json
```

```json
{
  "type": "https://imsu.io/docs/mcp#authentication",
  "title": "Authorization required",
  "status": 401,
  "detail": "Connect with OAuth: MCP clients start it from the resource_metadata URL in WWW-Authenticate. Headless clients can send an API key from Settings → API keys as Authorization: Bearer imsu_pat_….",
  "traceId": "00-2efde6ac7f7c19f6e2d4d7cd3305f9d6-332da20ca9be86ae-00"
}
```

A token that is unknown, expired or revoked, or whose account is suspended, gets the same `401` with `error="invalid_token"` added to the challenge and the title "Invalid token". An MCP client then refreshes its access token, or signs in again if that fails. An OAuth token issued for another resource gets the title "Token not accepted here".

## Insufficient scope {#insufficient-scope}

On the consent page the user may allow reading only. A connection like that can call `list_monitors`, `get_monitor` and `get_account_limits`, but a call to a tool that changes monitors is refused before the tool runs:

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="monitors:read monitors:write", resource_metadata="https://imsu.io/.well-known/oauth-protected-resource/mcp"
Content-Type: application/problem+json
```

```json
{
  "type": "https://imsu.io/docs/mcp#insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This tool changes monitors, which needs the monitors:write scope. Connect to imsu.io again and allow it to create, change, pause and delete monitors.",
  "traceId": "00-42da06f26ce861e15a29fa3ac5ed5672-9838b78501419194-00"
}
```

The client should tell the user and run the authorization again with `scope=monitors:read monitors:write` (a step-up). The user then sees the consent page again and can allow changes.

An API key can't be re-authorized, so a read-only key gets a tool result with `isError: true` instead: "This API key can only read monitors. To let the agent change them, create a key with read and write access in Settings → API keys on imsu.io and use it instead."

## Rate limits {#rate-limits}

Each credential (a connected app or an API key) may send 120 requests a minute to `/mcp`. This budget is separate from the REST API's. Over it, the answer is `429` with `Retry-After` in seconds:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 37
Content-Type: application/problem+json
```

```json
{
  "type": "https://imsu.io/docs/mcp#rate-limits",
  "title": "Too many requests",
  "status": 429,
  "detail": "This credential sent more requests than its limit allows. Wait 37 seconds (Retry-After) and try again.",
  "traceId": "00-6b0f9a2c1d7e4f3a8b5c2d1e0f9a8b7c-1a2b3c4d5e6f7a8b-00"
}
```

Wait that many seconds, then retry. The plan's limits still cap how many monitors there can be.

## Body too large {#body-too-large}

A request body can be at most 256 KB; a tool call takes a few hundred bytes. A larger one gets `413`:

```json
{
  "type": "https://imsu.io/docs/mcp#body-too-large",
  "title": "Request body too large",
  "status": 413,
  "detail": "The body can be at most 256 KB.",
  "traceId": "00-921cbe558ba8d0a6f9350f597eaa64f4-50c075d511578f06-00"
}
```

## Other HTTP answers

- `GET` and `DELETE` on `/mcp` get `405 Method Not Allowed` with `Allow: POST`: the server keeps no sessions or streams. Clients of 2025-06-18 and 2025-11-25 take this as "carry on without a stream".
- A JSON-RPC batch (a JSON array) and an unknown `MCP-Protocol-Version` header get `400` with a JSON-RPC error.
- A body in a charset the server can't read gets `415` "Unsupported charset". Send UTF-8 JSON.
- Browser-based clients may call from any origin: preflights are answered without a credential, and `WWW-Authenticate` and `Retry-After` are readable.

## Tool results and errors

Each tool returns `structuredContent` that matches its output schema, plus two text blocks for clients that only show text: a sentence for people, then the same result as JSON. For example, `create_monitor` answers:

```text
Created "Shop" (https://shop.example.com/health), checked every 5 min. First check within a minute. id: 01a1c6de-07ae-72e1-9b05-b184f79ad5ec
```

```json
{
  "id": "01a1c6de-07ae-72e1-9b05-b184f79ad5ec",
  "name": "Shop",
  "type": "http",
  "target": "https://shop.example.com/health",
  "status": "unknown",
  "is_paused": false,
  "interval_seconds": 300,
  "url": "https://shop.example.com/health",
  "method": "GET",
  "timeout_seconds": 10,
  "expected_status": "200-299",
  "keyword": "ok",
  "follow_redirects": true,
  "failure_threshold": 2,
  "host": null,
  "port": null,
  "expect_closed": null,
  "grace_seconds": null,
  "run_timeout_seconds": null,
  "ping_url": null,
  "last_ping_at": null,
  "uptime_24h": null,
  "last_check_at": null,
  "open_incident": null,
  "created_at": "2026-11-10T19:10:02.212627+00:00"
}
```

Monitors come back in snake_case. Settings of other types are `null`. `status` is `up`, `down`, `unknown` (not checked yet, or a heartbeat without its first ping) or `paused`. `open_incident` is `{ "started_at", "cause" }` while the monitor is down.

Problems the model can fix come back as a tool result with `isError: true` and one sentence that says how to fix the call. Arguments are checked against the tool's input schema first, so a wrong name or type is named:

```text
Invalid arguments. interval_minutes is not an argument of create_monitor, which takes: name, type, url, host, port, interval_seconds, timeout_seconds, expected_status, keyword, method, follow_redirects, failure_threshold, expect_closed, grace_seconds, run_timeout_seconds, allow_duplicate. Correct them and call create_monitor again.
```

```text
Invalid arguments. interval_seconds: Interval must be between 300 and 86400 seconds on your plan. Correct them and call create_monitor again.
```

```text
No monitor with id dd48baea-6ec5-4c2f-b3b3-ef3fc26f2e22 in this account. Call list_monitors to find the right id.
```

```text
The account's plan allows 10 monitors, and all 10 are in use (paused ones count). Tell the user, who can delete a monitor they no longer need (higher limits are set by imsu.io staff); get_account_limits shows the numbers.
```

Results contain only the account's own names and settings and errors imsu.io wrote, such as "Timed out after 10 s". They never contain what a monitored site sent.

## Tools

The list below is generated from the server's `tools/list`, in the order the server lists the tools. "Read-only" tools change nothing; "destructive" means the change can't be undone; "idempotent" means calling again with the same arguments changes nothing more; "closed world" means the tool only touches the imsu.io account. Tools that change monitors need the `monitors:write` scope; the others need `monitors:read`.

<!-- tools:start -->
<!-- Generated from the server's tools/list by DocsTests (tests/Imsu.Api.Tests); don't edit it by hand. To update it, run those tests with IMSU_UPDATE_DOCS=1. -->

### Server instructions

The server sends these to the client when it connects:

> imsu.io checks websites (http), TCP ports (port) and cron jobs (heartbeat) on a schedule and alerts the account owner when they go down. Monitor ids are UUIDs: find them with list_monitors; never guess. Intervals are in seconds and can't be shorter than the account's minimum (get_account_limits). Pausing stops checks and alerts; deleting removes the monitor and hides its history. Ask the user before deleting.

### list_monitors

**List monitors.** Lists the account's monitors with their id, name, type, target, status and uptime over the last 24 hours, and counts every monitor by status. Use it to find a monitor's id. Filters combine. Monitors come oldest first, a page at a time: when next_cursor is not null, call again with that cursor for the next page.

- Scope: `monitors:read`
- Annotations: read-only, idempotent, closed world
- Arguments:
  - `status` (one of `up`, `down`, `unknown`, `paused`; optional): Only monitors in this state: up, down, unknown (not checked yet, or a heartbeat without its first ping) or paused.
  - `type` (one of `http`, `port`, `heartbeat`; optional): Only monitors of this type: http (websites and APIs), port (TCP ports) or heartbeat (jobs that ping imsu.io).
  - `search` (string; optional): Text to find in the name or the target (URL or host:port), ignoring case.
  - `limit` (integer; optional, default `50`): How many monitors to return, 1 to 200. Default 50.
  - `cursor` (string; optional): The next_cursor of the previous call, for the next page. Leave it out for the first page.
  - `ssl_expiring_within_days` (integer; optional): Only monitors whose SSL certificate expires within this many days, or has expired, e.g. 30.

### get_monitor

**Get a monitor.** Gets one monitor: all its settings, its status, its uptime over the last 24 hours, the open incident while it is down, and for heartbeats the URL their job must ping.

- Scope: `monitors:read`
- Annotations: read-only, idempotent, closed world
- Arguments:
  - `id` (UUID; required): The monitor's id, a UUID. Find it with list_monitors; never guess it.

### get_account_limits

**Get account limits.** Gets the account's plan limits: how many monitors it may have, how many it has, and the shortest interval_seconds allowed. Call it when unsure whether a monitor fits or how often it may be checked.

- Scope: `monitors:read`
- Annotations: read-only, idempotent, closed world
- Arguments: none

### create_monitor

**Create a monitor.** Creates a monitor and starts checking it; the account owner is alerted when it goes down. Types: http requests a URL (needs url), port connects to a TCP port (needs host and port), heartbeat waits for a job to request its ping_url at least every interval_seconds. An http or port monitor is first checked within a minute; a heartbeat's first ping is due within interval_seconds plus grace_seconds. Settings left out get their defaults. Refuses a second monitor of the same type for the same target (heartbeats: with the same name) unless allow_duplicate is true.

- Scope: `monitors:write`
- Annotations: changes monitors, not destructive, not idempotent, closed world
- Arguments:
  - `name` (string; required): A short name people recognize, at most 100 characters, e.g. "Shop" or "Nightly backup".
  - `type` (one of `http`, `port`, `heartbeat`; optional, default `http`): What to check: http (a URL; the default), port (a TCP port) or heartbeat (a cron job or worker that pings imsu.io). Can't be changed later.
  - `url` (string; optional): HTTP only: the absolute http:// or https:// URL to request, e.g. https://shop.example.com/health. Required for http.
  - `host` (string; optional): Port only: a domain name or IP address, without a scheme or path, e.g. db.example.com. Required for port.
  - `port` (integer; optional): Port only: the TCP port, 1 to 65535, e.g. 5432. Required for port.
  - `interval_seconds` (integer; optional): Seconds between checks: 60 = 1 minute, 300 = 5 minutes, 3600 = 1 hour, 86400 = 1 day (the most). For heartbeats, the longest time expected between pings, up to 2678400 (31 days). At least the account's minimum interval; call get_account_limits if unsure. Default: the account's minimum.
  - `timeout_seconds` (integer; optional): HTTP and port: seconds to wait for an answer before the check fails, 1 to 30, and shorter than the interval. Default 10.
  - `expected_status` (string; optional): HTTP only: the status codes that count as up, as codes and ranges, e.g. "200-299" or "200,301". Default "200-299".
  - `keyword` (string; optional): HTTP only: text the response body must contain (case-sensitive, at most 200 characters), or the check fails. Not possible with method HEAD. Default: none.
  - `method` (string; optional): HTTP only: GET or HEAD. Default GET.
  - `follow_redirects` (boolean; optional): HTTP only: follow redirects and check the final response. Default true.
  - `failure_threshold` (integer; optional): HTTP and port: failed checks in a row before the monitor is down and alerts go out, 1 to 10. Default 2.
  - `expect_closed` (boolean; optional): Port only: a reverse check that alerts when the port accepts connections, e.g. for a database that must not be reachable from the internet. Default false.
  - `grace_seconds` (integer; optional): Heartbeat only: how many seconds a ping may be late, on top of interval_seconds, before the monitor is down; 0 to 86400. Default 300 (5 minutes).
  - `run_timeout_seconds` (integer; optional): Heartbeat only: how many seconds a run that reports progress may take to reach 100%; 60 to 86400. Default 3600 (1 hour).
  - `ssl_expiry_alerts` (boolean; optional): HTTP with an https URL only: warn by email and chat before the SSL certificate expires. Default true for https URLs.
  - `ssl_alert_days` (integer; optional): HTTP only: days before the certificate expires that the first warning goes out: 1, 3, 7, 14 or 30. Another follows at each of 14, 7, 3 and 1 days left below it. Default 14.
  - `allow_duplicate` (boolean; optional, default `false`): Create the monitor even if one of the same type already checks the same target (heartbeats: has the same name). Pass true only when the user really wants a second monitor. Default false.

### update_monitor

**Change a monitor.** Changes some of a monitor's settings: pass its id and only the settings to change; the others keep their values. The type can't change: create a new monitor instead. Settings of another type than the monitor's are ignored.

- Scope: `monitors:write`
- Annotations: changes monitors, not destructive, idempotent, closed world
- Arguments:
  - `id` (UUID; required): The monitor's id, a UUID. Find it with list_monitors; never guess it.
  - `name` (string; optional): A short name people recognize, at most 100 characters, e.g. "Shop" or "Nightly backup".
  - `url` (string; optional): HTTP only: the absolute http:// or https:// URL to request, e.g. https://shop.example.com/health.
  - `host` (string; optional): Port only: a domain name or IP address, without a scheme or path, e.g. db.example.com.
  - `port` (integer; optional): Port only: the TCP port, 1 to 65535, e.g. 5432.
  - `interval_seconds` (integer; optional): Seconds between checks: 60 = 1 minute, 300 = 5 minutes, 3600 = 1 hour, 86400 = 1 day (the most). For heartbeats, the longest time expected between pings, up to 2678400 (31 days). At least the account's minimum interval; call get_account_limits if unsure.
  - `timeout_seconds` (integer; optional): HTTP and port: seconds to wait for an answer before the check fails, 1 to 30, and shorter than the interval.
  - `expected_status` (string; optional): HTTP only: the status codes that count as up, as codes and ranges, e.g. "200-299" or "200,301".
  - `keyword` (string; optional): HTTP only: text the response body must contain (case-sensitive, at most 200 characters), or the check fails. Not possible with method HEAD. null or "" removes the keyword.
  - `method` (string; optional): HTTP only: GET or HEAD.
  - `follow_redirects` (boolean; optional): HTTP only: follow redirects and check the final response.
  - `failure_threshold` (integer; optional): HTTP and port: failed checks in a row before the monitor is down and alerts go out, 1 to 10.
  - `expect_closed` (boolean; optional): Port only: a reverse check that alerts when the port accepts connections, e.g. for a database that must not be reachable from the internet.
  - `grace_seconds` (integer; optional): Heartbeat only: how many seconds a ping may be late, on top of interval_seconds, before the monitor is down; 0 to 86400.
  - `run_timeout_seconds` (integer; optional): Heartbeat only: how many seconds a run that reports progress may take to reach 100%; 60 to 86400.
  - `ssl_expiry_alerts` (boolean; optional): HTTP with an https URL only: warn by email and chat before the SSL certificate expires.
  - `ssl_alert_days` (integer; optional): HTTP only: days before the certificate expires that the first warning goes out: 1, 3, 7, 14 or 30. Another follows at each of 14, 7, 3 and 1 days left below it.

### pause_monitor

**Pause a monitor.** Pauses a monitor: its checks and alerts stop until it is resumed. Its settings and history are kept, and it still counts toward the plan's monitors. Pausing a paused monitor changes nothing.

- Scope: `monitors:write`
- Annotations: changes monitors, not destructive, idempotent, closed world
- Arguments:
  - `id` (UUID; required): The monitor's id, a UUID. Find it with list_monitors; never guess it.

### resume_monitor

**Resume a monitor.** Resumes a paused monitor: checks and alerts start again. An http or port monitor is checked right away; a heartbeat waits interval_seconds plus grace_seconds for its job's next ping. Resuming an active monitor changes nothing.

- Scope: `monitors:write`
- Annotations: changes monitors, not destructive, idempotent, closed world
- Arguments:
  - `id` (UUID; required): The monitor's id, a UUID. Find it with list_monitors; never guess it.

### delete_monitor

**Delete a monitor.** Deletes a monitor: its checks and alerts stop, and its history is no longer shown. This can't be undone. Ask the user to confirm first, naming the monitor. To stop checks for a while, pause it instead.

- Scope: `monitors:write`
- Annotations: changes monitors, destructive, idempotent, closed world
- Arguments:
  - `id` (UUID; required): The monitor's id, a UUID. Find it with list_monitors; never guess it.

<!-- tools:end -->

## Example conversations

What the model calls, and what the tools answer. Ids and ping URLs differ in your account.

### "Watch my shop's health endpoint every 5 minutes and check it says ok"

```json
{ "name": "create_monitor", "arguments": { "name": "Shop", "url": "https://shop.example.com/health", "interval_seconds": 300, "keyword": "ok" } }
```

```text
Created "Shop" (https://shop.example.com/health), checked every 5 min. First check within a minute. id: 01a1c6de-07ae-72e1-9b05-b184f79ad5ec
```

### "Alert me if the database port is ever open to the internet"

```json
{ "name": "create_monitor", "arguments": { "name": "Database", "type": "port", "host": "db.example.com", "port": 5432, "expect_closed": true } }
```

```text
Created "Database" (db.example.com:5432), checked every 5 min. First check within a minute. id: 01a1c6de-084f-7aef-a79d-2b97f1373433
```

### "Tell me if my nightly backup stops running"

```json
{ "name": "create_monitor", "arguments": { "name": "Nightly backup", "type": "heartbeat", "interval_seconds": 86400 } }
```

```text
Created the heartbeat "Nightly backup": it is down when no ping arrives for 1 day plus 5 min of grace. The job must request https://imsu.io/hb/CsiKMWtrqPBR6x39mVXvFw after each run, e.g. with this crontab line, which runs it every 1 day:
0 3 * * * /path/to/job.sh && curl -fsS -m 10 --retry 3 https://imsu.io/hb/CsiKMWtrqPBR6x39mVXvFw > /dev/null
id: 01a1c6de-086f-7a31-bb4f-1e4ead467e8c
```

The model then shows the user the crontab line. The job can also report a failed run by requesting the ping URL with `/fail` at the end. For `run_timeout_seconds` to apply, the job must report each run's progress: see [heartbeat pings](https://imsu.io/docs/api#heartbeat-pings).

### "Is anything down?"

```json
{ "name": "list_monitors", "arguments": { "status": "down" } }
```

```text
The account has 3 monitors: 2 up, 1 down, 0 unknown, 0 paused. Showing 1:
- "Shop" (https://shop.example.com/health): down, every 5 min, 97.92% up in 24 h. id: 01a1c6de-07ae-72e1-9b05-b184f79ad5ec
```

For the cause, the model calls `get_monitor` with that id and reads `open_incident`.

### "Rename the shop monitor and drop the keyword check"

```json
{ "name": "update_monitor", "arguments": { "id": "01a1c6de-07ae-72e1-9b05-b184f79ad5ec", "name": "Shop health", "keyword": null } }
```

```text
Saved "Shop health" (https://shop.example.com/health), checked every 5 min. id: 01a1c6de-07ae-72e1-9b05-b184f79ad5ec
```

Settings left out keep their values; `"keyword": null` removes the keyword.

### "Pause the shop monitor while we deploy"

```json
{ "name": "pause_monitor", "arguments": { "id": "01a1c6de-07ae-72e1-9b05-b184f79ad5ec" } }
```

```text
Paused "Shop health" (https://shop.example.com/health): no checks or alerts until it is resumed. id: 01a1c6de-07ae-72e1-9b05-b184f79ad5ec
```

Later, `resume_monitor` with the same id: "Resumed "Shop health" (https://shop.example.com/health), checked every 5 min; the first check runs now."

### "Delete the shop monitor"

The model first asks: "Delete "Shop health" (https://shop.example.com/health)? Its history will no longer be shown." Only after the user says yes:

```json
{ "name": "delete_monitor", "arguments": { "id": "01a1c6de-07ae-72e1-9b05-b184f79ad5ec" } }
```

```text
Deleted "Shop health" (https://shop.example.com/health). Its checks and alerts have stopped, and its history is no longer shown.
```

---

URL: https://imsu.io/docs/authentication
Markdown: https://imsu.io/docs/authentication.md

# Authentication for MCP clients

imsu.io's MCP server at `https://imsu.io/mcp` is protected by an OAuth 2.1 authorization server at `https://imsu.io`. This page is for people who build MCP clients, and for agents that want to know what their client does. If you only want to connect a client, see [MCP server](https://imsu.io/docs/mcp); MCP clients do all of this for you.

What imsu.io supports, in short:

- The MCP authorization spec (revisions 2025-06-18, 2025-11-25 and 2026-07-28): a `401` with `resource_metadata`, Protected Resource Metadata (RFC 9728) and Authorization Server Metadata (RFC 8414).
- The authorization code grant with PKCE (S256 only) and refresh tokens. Public clients only: there are no client secrets.
- Clients identify themselves with a Client ID Metadata Document (an HTTPS URL as `client_id`) or register with Dynamic Client Registration (RFC 7591).
- Resource indicators (RFC 8707): tokens are issued for `https://imsu.io/mcp` only.
- `iss` in the authorization response (RFC 9207).
- No implicit flow, password grant, `plain` PKCE, client secrets, JWT access tokens or OpenID Connect.

Scripts and headless agents don't need any of this: they can send an API key ([REST API](https://imsu.io/docs/api#api-keys)), which `/mcp` accepts too.

## Metadata URLs

| Document | URL |
|---|---|
| Protected Resource Metadata of the MCP server | `https://imsu.io/.well-known/oauth-protected-resource/mcp` |
| The same, for clients that look at the root | `https://imsu.io/.well-known/oauth-protected-resource` |
| Authorization Server Metadata | `https://imsu.io/.well-known/oauth-authorization-server` |

They need no credential, may be read from any origin, and are cacheable for an hour (`Cache-Control: public, max-age=3600`). There is no `openid-configuration`.

## The flow, request by request

### 1. Call the MCP server without a token

```http
POST /mcp HTTP/1.1
Host: imsu.io
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
```

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://imsu.io/.well-known/oauth-protected-resource/mcp", scope="monitors:read monitors:write"
Content-Type: application/problem+json
```

The body is described in [MCP server](https://imsu.io/docs/mcp#authentication).

### 2. Read the protected resource metadata

```http
GET /.well-known/oauth-protected-resource/mcp HTTP/1.1
Host: imsu.io
```

```json
{
  "resource": "https://imsu.io/mcp",
  "authorization_servers": ["https://imsu.io"],
  "scopes_supported": ["monitors:read", "monitors:write"],
  "bearer_methods_supported": ["header"],
  "resource_name": "imsu.io",
  "resource_documentation": "https://imsu.io/docs/mcp"
}
```

### 3. Read the authorization server metadata

```http
GET /.well-known/oauth-authorization-server HTTP/1.1
Host: imsu.io
```

```json
{
  "issuer": "https://imsu.io",
  "authorization_endpoint": "https://imsu.io/oauth/authorize",
  "token_endpoint": "https://imsu.io/oauth/token",
  "registration_endpoint": "https://imsu.io/oauth/register",
  "revocation_endpoint": "https://imsu.io/oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "revocation_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["monitors:read", "monitors:write"],
  "client_id_metadata_document_supported": true,
  "authorization_response_iss_parameter_supported": true,
  "service_documentation": "https://imsu.io/docs/authentication"
}
```

### 4. Get a client id

Use the URL of your [client metadata document](#client-id-metadata-documents) as `client_id`, or register. Registering a native app that listens on a loopback port:

```http
POST /oauth/register HTTP/1.1
Host: imsu.io
Content-Type: application/json

{"client_name":"My agent","redirect_uris":["http://127.0.0.1:33418/callback"]}
```

```http
HTTP/1.1 201 Created
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
```

```json
{
  "client_id": "imsu_client_6nYmKDC0DnLQnAtPUhFKCM",
  "client_id_issued_at": 1794337800,
  "client_name": "My agent",
  "redirect_uris": ["http://127.0.0.1:33418/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

### 5. Make a PKCE pair

The `code_verifier` is 43 to 128 characters of `A-Z a-z 0-9 - . _ ~`, e.g. 32 random bytes in base64url. The `code_challenge` is `BASE64URL(SHA-256(code_verifier))` without padding. The pair from RFC 7636:

```text
code_verifier  = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
```

### 6. Send the user's browser to the authorization endpoint

```text
https://imsu.io/oauth/authorize?response_type=code&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&scope=monitors%3Aread%20monitors%3Awrite&state=af0ifjsldkj&resource=https%3A%2F%2Fimsu.io%2Fmcp
```

| Parameter | |
|---|---|
| `response_type` | Required: `code` |
| `client_id` | Required: a registered id or a metadata document URL |
| `redirect_uri` | Required: one of the client's redirect URIs ([how they match](#redirect-uris)) |
| `code_challenge` | Required: the S256 challenge |
| `code_challenge_method` | Required: `S256` |
| `scope` | Optional: `monitors:read`, `monitors:write` or both, space-separated ([scopes](#scopes)) |
| `state` | Optional; sent back unchanged. PKCE already ties the answer to the request |
| `resource` | Optional; if sent, `https://imsu.io/mcp` |

Each parameter may be sent once (`resource` may repeat with the same value). imsu.io checks the request and sends the browser to its consent page, inside a sealed handle that is valid for 10 minutes:

```http
HTTP/1.1 302 Found
Location: https://imsu.io/authorize?request=CfDJ8EMIfNIR89tLlY866dBoCic4aI0Fq2yp95Dzogt9H3O…
```

If the request is wrong, see [errors at the authorization endpoint](#errors-at-the-authorization-endpoint).

### 7. The user signs in and allows access

If the user isn't signed in, imsu.io asks them to sign in (or sign up) first and then brings them back. The consent page names the app, says where the browser will return to, and lists what the app may do:

- See your monitors and their status (`monitors:read`, always included)
- Create, change, pause and delete monitors (`monitors:write`, if asked for; the user may untick it)

Apps registered with Dynamic Client Registration are marked "imsu.io hasn't verified this app", because anyone can register an app under any name. Apps known by their metadata URL show that URL's host instead. No logos are shown.

When the user clicks **Allow**, the browser goes back to the client with a single-use code:

```text
http://127.0.0.1:33418/callback?code=imsu_ac_MoogF2zjfJ7JQKXxgg3YtY97erq1xcrkmUw02JEchmj&state=af0ifjsldkj&iss=https%3A%2F%2Fimsu.io
```

Check that `iss` is `https://imsu.io`. When the user clicks **Cancel**, the client gets `error=access_denied` with `state` and `iss`.

### 8. Exchange the code for tokens

The code works once, within 60 seconds.

```http
POST /oauth/token HTTP/1.1
Host: imsu.io
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=imsu_ac_MoogF2zjfJ7JQKXxgg3YtY97erq1xcrkmUw02JEchmj&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&resource=https%3A%2F%2Fimsu.io%2Fmcp
```

```http
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
```

```json
{
  "access_token": "imsu_at_H4zN5w3m47PM075l0Hpaj8thgf5yjNtKndxdfJtEORw",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "imsu_rt_amp3sWE4h6uIq3OZUHRrHA2CLHnLN9doHtRIbiyrCJH",
  "scope": "monitors:read monitors:write"
}
```

`redirect_uri` may be left out, as OAuth 2.1 allows; if you send it, it must be the one from step 6. `scope` says what the user allowed, which may be less than you asked for.

### 9. Call the MCP server with the access token

```http
POST /mcp HTTP/1.1
Host: imsu.io
Authorization: Bearer imsu_at_H4zN5w3m47PM075l0Hpaj8thgf5yjNtKndxdfJtEORw
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
```

```http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache,no-store
```

```text
event: message
data: {"result":{"protocolVersion":"2025-11-25","capabilities":{"logging":{},"tools":{}},"serverInfo":{"name":"imsu.io","title":"imsu.io","version":"1.0.0"},"instructions":"imsu.io checks websites (http), TCP ports (port) and cron jobs (heartbeat) on a schedule … Ask the user before deleting."},"id":1,"jsonrpc":"2.0"}
```

The answer may come as an event stream of one event whose `data:` line holds the JSON-RPC message, or as plain JSON; accept both. There is no `Mcp-Session-Id`: the server keeps no session, so send the access token and `MCP-Protocol-Version` with every later request.

The token works only in the `Authorization` header and only at `https://imsu.io/mcp`; the REST API at `/api/v1` refuses it with `401`.

### 10. Refresh the access token

Access tokens last 1 hour. Before or after one expires, trade the refresh token for new tokens:

```http
POST /oauth/token HTTP/1.1
Host: imsu.io
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=imsu_rt_amp3sWE4h6uIq3OZUHRrHA2CLHnLN9doHtRIbiyrCJH&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM
```

```json
{
  "access_token": "imsu_at_IFFtxW7u0V8QwjJdVILXm7c3SCQeoYA74NpajGgEeIJ",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "imsu_rt_o1owcrDfMAIclUkzXPdP5sANxYwIa8GfxrCK3oR65fM",
  "scope": "monitors:read monitors:write"
}
```

Keep the new refresh token and forget the old one: see [refresh token rotation](#refresh-token-rotation). Optional parameters: `scope`, to narrow the new access token (the refresh token keeps what was granted), and `resource` (`https://imsu.io/mcp`).

### 11. Revoke tokens when the user disconnects

```http
POST /oauth/revoke HTTP/1.1
Host: imsu.io
Content-Type: application/x-www-form-urlencoded

token=imsu_rt_o1owcrDfMAIclUkzXPdP5sANxYwIa8GfxrCK3oR65fM&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM&token_type_hint=refresh_token
```

```http
HTTP/1.1 200 OK
Cache-Control: no-store
```

Revoking a refresh token ends the whole connection, with every token issued for it; revoking an access token ends only that token. Any well-formed request gets an empty `200`, also for a token that is unknown, already revoked or another client's (then nothing is revoked). A request without `token` or `client_id` gets `400 invalid_request`.

## Client ID Metadata Documents

A client can use an HTTPS URL as its `client_id`, pointing at a JSON document it publishes. imsu.io then needs no registration, and the consent page shows the URL's host, so users can tell who the app is.

```json
{
  "client_id": "https://app.example.com/oauth/client.json",
  "client_name": "Example Agent",
  "client_uri": "https://app.example.com",
  "redirect_uris": ["https://app.example.com/oauth/callback", "http://127.0.0.1/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

- The URL must be HTTPS with a path, without a fragment, user name, password or `.`/`..` segments, and at most 2048 characters.
- imsu.io fetches it with `Accept: application/json`, from public addresses only, without following redirects, within 5 seconds. The document must be a JSON object of at most 5 KB.
- `client_id` must equal the URL exactly; `redirect_uris` must hold 1 to 10 valid [redirect URIs](#redirect-uris); `token_endpoint_auth_method` must be absent or `none`. `client_name` defaults to the URL's host.
- The document is cached as its `Cache-Control: max-age` says, at least 5 minutes and at most 24 hours. When the cached copy has expired and can't be fetched again, the authorization fails (`client_metadata_unavailable`); a stale copy is never used.
- Hosts are shown in their ASCII form (`xn--…` for internationalized names), so a look-alike name can't pass for another.

## Dynamic Client Registration

`POST https://imsu.io/oauth/register` takes a JSON body of at most 16 KB and registers a public client ([example](#4-get-a-client-id)).

- `redirect_uris` (required): 1 to 10 [redirect URIs](#redirect-uris).
- `client_name`: trimmed, without control characters, at most 100 characters; "Unnamed app" if missing.
- `client_uri`: kept only if it is HTTPS. `logo_uri` is ignored: imsu.io shows no logos.
- `grant_types` and `response_types` may only list `authorization_code`, `refresh_token` and `code`.
- `token_endpoint_auth_method` is always registered as `none`; no secret is issued.
- Registered apps are marked unverified on the consent page. Registrations that are never used to connect are deleted after 7 days.
- Each address may register 10 clients an hour; then `429` with `Retry-After`.

Errors are `400` with `{ "error", "error_description" }`: `invalid_redirect_uri` for a redirect URI that isn't allowed, `invalid_client_metadata` for anything else.

```json
{
  "error": "invalid_redirect_uri",
  "error_description": "An http redirect URI is only allowed on a loopback host (127.0.0.1, [::1] or localhost). Use https."
}
```

## Redirect URIs

A redirect URI is an absolute URI without a fragment, and one of:

- HTTPS, e.g. `https://app.example.com/oauth/callback`;
- HTTP on a loopback host: `http://127.0.0.1`, `http://[::1]` or `http://localhost`;
- an app's own scheme, e.g. `cursor://anysphere.cursor-mcp/oauth/callback` or `com.example.app:/callback`.

Schemes such as `javascript:`, `data:`, `file:`, `ws:` and `ftp:` are refused. The `redirect_uri` of an authorization request must match a registered one exactly, except that on a loopback host any port matches, because native apps listen on a port they pick at run time. Scheme, host, path and query must still match.

## PKCE

PKCE is required, with `code_challenge_method=S256`; `plain` is refused. The token request must send the `code_verifier` whose S256 hash is the `code_challenge` of the authorization request ([example](#5-make-a-pkce-pair)).

## Resource

The only resource is `https://imsu.io/mcp`. `resource` is optional in the authorization and token requests; if sent, it must be that value, or the answer is `invalid_target`. Every token is issued for it, and other resources refuse it.

## Scopes

| Scope | Allows |
|---|---|
| `monitors:read` | List and read monitors and the plan's limits |
| `monitors:write` | Create, change, pause, resume and delete monitors; includes `monitors:read` |

Scopes imsu.io doesn't know (`openid`, `offline_access`, …) are ignored, and asking for none of these two, or leaving `scope` out, asks for both. The user may allow reading only. Such a connection gets a `403 insufficient_scope` challenge when it calls a tool that changes monitors; ask again with both scopes to step up ([details](https://imsu.io/docs/mcp#insufficient-scope)).

## Token lifetimes

| | Format | Lifetime |
|---|---|---|
| Authorization code | `imsu_ac_…` | 60 seconds, once |
| Access token | `imsu_at_…` | 1 hour (`expires_in: 3600`) |
| Refresh token | `imsu_rt_…` | 30 days, replaced on every use |
| Consent request | sealed `request` handle | 10 minutes to sign in and allow |
| Connection (grant) | | Until the user disconnects it, the app revokes it, or a reused code or refresh token ends it |

Tokens are opaque: don't parse them. imsu.io stores only their SHA-256 hashes. Connecting the same app again for the same user and account replaces the old connection and its tokens.

## Refresh token rotation

Every refresh returns a new refresh token, and the old one stops working. If an old refresh token is presented again, imsu.io assumes it was stolen and ends the connection with all its tokens:

```json
{
  "error": "invalid_grant",
  "error_description": "The refresh token was already used, so the access it gave was revoked. Connect the app again."
}
```

The same happens when an authorization code is used twice. Store the newest refresh token before using the new access token, and don't refresh from two places at once.

## Disconnecting

The user sees every connected app at [https://imsu.io/settings](https://imsu.io/settings), under Connected apps, with what it may do and what it changed recently, and can disconnect it there. Disconnecting, or the app revoking its refresh token, ends all of the connection's tokens at once: the next request gets `401`. While the account is suspended, or once the user is no longer a member of it, its tokens stop working too.

## Errors at the authorization endpoint

While imsu.io can't trust where to send the browser (the client or its redirect URI is unknown or doesn't match), it shows its own error page instead: `https://imsu.io/authorize?error=<code>`.

| Code | Meaning |
|---|---|
| `invalid_request` | `client_id` or `redirect_uri` is missing or sent twice |
| `unknown_client` | No registered client has this `client_id` |
| `client_metadata_unavailable` | The client metadata document couldn't be fetched, or isn't valid |
| `invalid_redirect_uri` | `redirect_uri` isn't one of the client's |

Once client and redirect URI are known, other errors go back to a native app's redirect URI (loopback HTTP or its own scheme) with `error`, `error_description`, `state` and `iss`; an HTTPS redirect URI gets the error page instead, so imsu.io can't be used to send people to any site:

```text
http://127.0.0.1:33418/callback?error=invalid_request&error_description=code_challenge_method%20must%20be%20S256;%20plain%20is%20not%20supported.&state=af0ifjsldkj&iss=https%3A%2F%2Fimsu.io
```

| `error` | Meaning |
|---|---|
| `invalid_request` | PKCE is missing or malformed, the method isn't `S256`, `response_type` is missing, or a parameter was sent twice |
| `unsupported_response_type` | `response_type` isn't `code` |
| `invalid_target` | `resource` isn't `https://imsu.io/mcp` |
| `access_denied` | The user clicked Cancel |

## Errors at the token endpoint

The token endpoint takes form-encoded bodies of at most 16 KB and answers errors as `{ "error", "error_description" }` with `Cache-Control: no-store`:

| Status and `error` | Meaning |
|---|---|
| `400 invalid_request` | A parameter is missing or sent twice, the body isn't form-encoded or is too large, or the `code_verifier` is malformed |
| `400 unsupported_grant_type` | `grant_type` isn't `authorization_code` or `refresh_token` |
| `401 invalid_client` | `client_id` is unknown |
| `400 invalid_grant` | The code or refresh token is unknown, expired, revoked, already used, or another client's; the `code_verifier` doesn't match; `redirect_uri` differs from the authorization request's |
| `400 invalid_target` | `resource` isn't the token's resource |
| `400 invalid_scope` | A refresh asked for a scope the user didn't grant |

## Rate limits

Per client address: 60 requests a minute to each of `/oauth/authorize`, `/oauth/token` and `/oauth/revoke`, and 10 registrations an hour at `/oauth/register`. Over a limit, the answer is `429` with `Retry-After` in seconds. The MCP server's own limit is per credential ([details](https://imsu.io/docs/mcp#rate-limits)).

## Troubleshooting

| Problem | Likely cause | Fix |
|---|---|---|
| The browser shows "imsu.io doesn't know this app" | The `client_id` was never registered, or the registration was deleted after 7 days unused | Register again, or use a client metadata document |
| "This app asked to return to an address it didn't register" | `redirect_uri` doesn't match a registered one | Send a registered redirect URI; on loopback only the port may differ |
| "imsu.io couldn't read this app's details" | The metadata document is unreachable, too large, slow, or invalid | Serve valid JSON of at most 5 KB from a public address, without redirects |
| "This sign-in request has expired" | More than 10 minutes passed between the authorization request and Allow | Start the authorization again |
| `invalid_grant` when exchanging the code | The code expired (60 seconds), was already used, or the `code_verifier` doesn't match | Exchange the code right away, once, with the verifier of the same request |
| `invalid_grant` "already used, so the access it gave was revoked" | A refresh token or code was used twice | Connect again, and keep only the newest refresh token |
| `401` from `/mcp` with `error="invalid_token"` | The access token expired or was revoked | Refresh it; if that fails, connect again |
| `401` "Token not accepted here" from `/api/v1` | OAuth tokens only work at `/mcp` | Use an API key for the REST API |
| `403 insufficient_scope` from `/mcp` | The user allowed reading only | Ask for `monitors:read monitors:write` again |
| `429` | Too many requests from one address or credential | Wait `Retry-After` seconds |

---

URL: https://imsu.io/docs/api
Markdown: https://imsu.io/docs/api.md

# 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](#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](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](https://imsu.io/docs/mcp) instead, which signs in with OAuth.

## API keys

### Create a key

1. Sign in at [https://imsu.io/settings](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.

```sh
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`](#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`](#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](#errors).
- 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](#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](#get-the-plans-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](#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](#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:

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

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

```json
{
  "user": {
    "id": "01a1c6de-b461-7461-919d-3d918e658f67",
    "email": "you@example.com",
    "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`

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

```json
{
  "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](#pagination)) |
| `sslExpiringWithinDays` | Only monitors whose SSL certificate expires within this many days, or has expired, e.g. `30` ([SSL certificate checks](#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.

```sh
curl "https://imsu.io/api/v1/monitors?type=http&q=shop" -H "Authorization: Bearer $IMSU_API_KEY"
```

```json
{
  "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](#fields-you-send); 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](#idempotency)): make a new key for each monitor, and send the same `$KEY` again when you retry this request.

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

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

```sh
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}'
```

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

```sh
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}'
```

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

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

```sh
curl https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745 -H "Authorization: Bearer $IMSU_API_KEY"
```

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

```sh
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}'
```

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

```sh
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}'
```

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

```sh
curl -X POST https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745/pause -H "Authorization: Bearer $IMSU_API_KEY"
```

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

```sh
curl -X POST https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745/resume -H "Authorization: Bearer $IMSU_API_KEY"
```

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

```sh
curl -X DELETE https://imsu.io/api/v1/monitors/01a1c6de-b61a-74a3-b8a8-8ec96fb2d745 -H "Authorization: Bearer $IMSU_API_KEY"
```

```http
HTTP/1.1 204 No Content
X-Request-Id: 0HNPVBP9D77OP
```

Deleting it again is [`404 not-found`](#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.

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

```json
{
  "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`](#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`](#idempotency-key-reused).
- While the first request is still running, a retry is [`409 idempotency-key-in-use`](#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
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`](#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.

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

```json
{
  "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 {#insufficient-scope}

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

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

```json
{
  "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 {#monitor-limit-reached}

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

```json
{
  "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 {#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](https://imsu.io/docs/mcp#tool-results-and-errors)). If `/api/v1` ever returns it, `existingMonitorId` names the monitor that already checks the target.

### 422 Idempotency key reused {#idempotency-key-reused}

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

```json
{
  "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 {#idempotency-key-in-use}

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

```json
{
  "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 {#rate-limited}

The API key made more requests this minute than its [limit](#rate-limits). `Retry-After` says how many seconds to wait.

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-Request-Id: 0HNPVBP9D78B7
Content-Type: application/problem+json
```

```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 {#body-too-large}

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

```json
{
  "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 {#unsupported-media-type}

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

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

---

URL: https://imsu.io/docs/bot
Markdown: https://imsu.io/docs/bot.md

# imsu.io bot

imsu.io is an uptime monitoring service. When an imsu.io user adds a monitor for a website, an API or a TCP port, our servers check it on a schedule and alert that user when it stops answering. This page describes those requests, so that site owners, firewalls and bot management services can recognize them.

## What it does {#purpose}

- It requests only the URLs and ports that imsu.io users enter themselves, usually their own sites and servers.
- It is not a crawler. It doesn't follow links, discover pages, index content, render pages or run JavaScript, and its results are not used to train AI models.
- It doesn't fetch `robots.txt`. Like other uptime monitors, it acts on behalf of a user who asked for one specific URL to be watched, so crawling rules don't apply to it.
- Each monitor is checked at most every 5 minutes on the free plan, and never more often than every 10 seconds. A failed check is retried once within 30 seconds.

## User agent {#user-agent}

HTTP checks send this `User-Agent` header:

```text
imsu.io-uptime-monitor/1.0 (+https://imsu.io)
```

The version number may change; match on `imsu.io-uptime-monitor`.

When an AI app asks an imsu.io user for access through OAuth, imsu.io fetches the app's published client metadata document once (and again when its cache expires). Those requests come from the same addresses and send:

```text
imsu.io-oauth/1.0 (+https://imsu.io)
```

## Requests {#requests}

- **HTTP checks:** one `GET` or `HEAD` request to the URL the user entered, with no request body and no credentials. Redirects are followed (up to 5) unless the user turned that off. The request times out after 1 to 30 seconds (10 by default). For a keyword check, at most the first 1 MB of the response body is read; otherwise only the status line and headers.
- **TCP port checks:** a TCP connection to the host and port, closed as soon as it is open. No data is sent.
- **Heartbeat monitors** don't contact your servers: your own job sends a request to imsu.io.
- Private and internal addresses (such as `10.0.0.0/8`, `192.168.0.0/16` and loopback) are never checked, including after a redirect or a DNS change.

## IP addresses {#ip-addresses}

Every check and every other request imsu.io makes comes from the addresses listed at:

```text
https://imsu.io/api/meta/ips
```

The list is JSON, needs no sign-in, and may be fetched from any origin. Cache it for up to an hour, and fetch it again from time to time: addresses are added there before new servers start sending requests. Today it holds one IPv4 address and one IPv6 prefix:

```json
{
  "ipv4": ["178.104.120.188/32"],
  "ipv6": ["2a01:4f8:1c18:c38f::/64"],
  "prefixes": [
    { "prefix": "178.104.120.188/32", "family": "ipv4", "service": "checks", "region": "europe" },
    { "prefix": "2a01:4f8:1c18:c38f::/64", "family": "ipv6", "service": "checks", "region": "europe" }
  ]
}
```

- `ipv4` and `ipv6` list the prefixes of each family in CIDR notation, for firewalls that take a plain list.
- `prefixes` lists them all, with what sends requests from each (`service`) and where those servers are (`region`).

To print the list for a firewall:

```sh
curl -s https://imsu.io/api/meta/ips | jq -r '.ipv4[], .ipv6[]'
```

## Verify a request {#verify}

The `User-Agent` header can be copied by anyone. A request is from imsu.io only if its source address is in one of the prefixes at `https://imsu.io/api/meta/ips`.

## Allow it {#allow}

If a firewall, a WAF or a bot protection service blocks the checks, the user's monitor reports your site as down. To let them through, allow the addresses from the list, ideally together with the user agent. On Cloudflare, for example, add a WAF custom rule that skips the security features for requests whose source IP is in the list.

## Stop it {#stop}

The checks come from an imsu.io user who added your site. If you don't want your site checked, block the addresses above or answer with an error; or write to [info@imsu.io](mailto:info@imsu.io) with the URL or host, and we will stop the monitors that check it. Write to the same address to report a problem with the bot.

---

URL: https://imsu.io/docs/privacy
Markdown: https://imsu.io/docs/privacy.md

# Privacy

Last updated: November 11, 2026.

This page says what personal data imsu.io collects, why, who else sees it, how long it is kept, and what you can ask for. It covers the website and web app, monitors and alerts, status pages, the REST API and the MCP server.

## Who runs imsu.io {#controller}

imsu.io is run by Burak Sarıca, an individual based in Turkey, who decides how your data is used (the data controller). Write to [info@imsu.io](mailto:info@imsu.io) about anything on this page.

imsu.io's servers and database are in the European Union.

## Summary

- We collect what the service needs: your email address, name and password (stored only as a hash), your monitors and their results, and the alert channels you connect.
- We don't sell personal data and don't use it for advertising.
- Google Analytics runs only if you accept it in the cookie banner.
- Apps you connect through MCP, and scripts that use your API keys, see what you allow them to see. What they do with it is up to them and their providers.
- You can ask for a copy of your data, a correction, or the deletion of your account by email.

## What we collect and why {#data}

### Your account

- **Email address, display name and password.** To create your account, sign you in and contact you about it. The password is stored only as a salted hash (PBKDF2); nobody, including us, can read it.
- **Sign-in times and email verification.** When you signed up, last signed in and verified your address, to keep the account secure.
- **Account membership and plan.** Which account you belong to, your role, and the account's plan and limits.

### Monitors, checks and alerts

- **Monitor settings.** Names, URLs, hosts and ports, intervals, keywords and the other settings you choose, to run the checks.
- **Check results.** For each check: when it ran, whether it was up, the status code, the response time and our own short error message (such as "timeout" or "unexpected status 503"). We don't store the response bodies of the sites we check.
- **Heartbeat pings.** When your job pinged its URL, and the run id and progress it sent. We don't store the IP address the ping came from.
- **Incidents.** When a monitor went down and came back, and why.
- **Alert channels.** Your alert email address, and the Slack webhook URL or the Telegram bot token and chat id you connect, to send alerts. Alerts carry the monitor's name, its target and the cause of the incident.
- **Sent alerts.** Who each alert went to, on which channel, and whether it was delivered.

The sites and ports we check see requests from imsu.io's servers ([imsu.io bot](https://imsu.io/docs/bot) lists their addresses and user agent). Any personal data in them is part of your own service; we act on your instructions when we check it.

### Status pages

- A status page you publish shows the names of its monitors, whether they are up, and their uptime over the last 24 hours to anyone with its link. It never shows the URLs, hosts or ports the monitors check.
- If you protect a page with a password, we store the password only as a salted hash.
- We don't keep a list of who visits a status page. A visitor who enters the password gets a cookie that keeps the page unlocked in that browser for 7 days (see [Cookies](#cookies)).

### API keys and the REST API

- **API keys.** Each key's name, scopes, first characters, expiry and when it was last used. The key itself is stored only as a hash and is shown to you once, when you create it.
- **Requests.** We don't keep a log of API requests or their bodies. If a request sends an `Idempotency-Key`, we keep a hash of the request and our response for 24 hours, so a retried request gets the same answer.

### Connected apps and the MCP server

When you connect an AI app (such as Claude, ChatGPT, Cursor or VS Code) to imsu.io through MCP, you sign in to imsu.io and choose what the app may do. Then:

- **About the app.** We store the app's name, website, return addresses and the other details it registered or published at its client ID URL. To read a published client ID URL, our server fetches it.
- **About the connection.** Who connected the app, to which account, with which permissions, when, and when it was last used. Access and refresh tokens are stored only as hashes. An access token works for 60 minutes and a refresh token for 30 days.
- **Tool calls.** We don't log the calls an app makes or their arguments. The results go to the app: your monitors' names, targets, settings and status, and heartbeat ping URLs. The app and its AI provider handle them under their own privacy terms, not ours. Connect only apps you trust, and disconnect them in Settings → Connected apps when you no longer use them.

### Changes made to monitors {#audit-log}

We record each change to a monitor (created, changed, paused, resumed, deleted): when, what changed, and who made it: you on the website, which API key, which connected app, or imsu.io support. You see this on the monitor's page and under Settings → Connected apps. It lets you check what an app or a script did.

### Server logs and security

- Our web server logs each request: the IP address, time, requested address, status code, referring page and browser (user agent). Sign-in addresses that carry one-time values are logged with those values removed. We use the logs to keep the service running and secure, and to investigate abuse.
- To slow down abuse, we count recent requests per IP address, API key or connected app in memory. These counts are not stored.
- Sign-in and registration use Cloudflare Turnstile to tell people from bots (see [Who else processes data](#processors)).

## Cookies and local storage {#cookies}

| Name | Where | What for | How long |
|---|---|---|---|
| `imsu.auth` | Cookie, imsu.io | Keeps you signed in. Required. | 14 days, or until you sign out |
| `imsu_status_<page>` | Cookie, sp.imsu.io | Keeps a password-protected status page unlocked. Required for that page. | 7 days |
| `imsu.signedIn` | Local storage | Remembers that you are signed in, so the home page doesn't flash before the app opens. Holds no account data. | Until you sign out |
| `imsu.consent` | Local storage | Remembers your answer to the cookie banner. | 12 months, then we ask again |
| `_ga`, `_ga_*` | Cookies, set by Google Analytics | Count visits and tell visitors apart. Only after you accept analytics. | Up to 2 years |

Change your answer at any time with **Cookie settings** at the bottom of the home page and the docs. If you turn analytics off, we delete its cookies in this browser.

With analytics on, Google Tag Manager loads Google Analytics, which records the pages you visit, the device and browser, an approximate location from the IP address, and a few product milestones: an account was created, its first monitor was made, it reached five monitors. These events carry no email addresses, names or URLs. We use them to see how people find and use imsu.io.

## Who else processes data {#processors}

We use these providers to run imsu.io. Each one gets only what its job needs.

| Provider | What for | What it gets |
|---|---|---|
| Our hosting provider (EU) | Servers and database | All the data above, stored in the European Union |
| [Resend](https://resend.com/legal/privacy-policy) | Sending alert and verification emails | Email addresses and the content of those emails |
| [Cloudflare Turnstile](https://www.cloudflare.com/privacypolicy/) | Telling people from bots at sign-in and registration | Your IP address and browser details |
| [Google Fonts](https://policies.google.com/privacy) | The site's fonts | Your IP address and browser details, when a page loads |
| [Google Analytics](https://policies.google.com/privacy) (through Google Tag Manager) | Visitor statistics | See [Cookies](#cookies). Only if you accept analytics |
| Slack, Telegram | Alerts, only if you connect them | The alert messages, sent to your webhook or bot |

Apps you connect through MCP, and the AI providers behind them, are not our providers: you choose them, and their own terms apply.

Some of these providers are based outside the European Union and Turkey, mostly in the United States. They process data under the safeguards their terms provide, such as the European Commission's Standard Contractual Clauses.

We share personal data with anyone else only if the law requires it.

## How long we keep data {#retention}

| Data | How long |
|---|---|
| Account details, monitors, incidents, alert channels | While your account exists |
| Raw check results, heartbeat pings and finished heartbeat runs | 30 days |
| Daily uptime figures | About 13 months (400 days) |
| Changes made to monitors | 400 days |
| `Idempotency-Key` records | 24 hours |
| Revoked API keys | 30 days after you revoke them |
| Ended OAuth tokens | 7 days after they expire or are revoked |
| Disconnected apps' connections | 30 days after you disconnect them |
| Apps nobody is connected to | 7 days |

Deleting a monitor stops its checks and alerts and hides it. Its raw checks and pings still age out on the schedule above; its settings and incidents stay until your account is deleted.

When you ask us to delete your account, we delete it and its data within 30 days, except what the law requires us to keep.

## Why we may use your data {#legal-bases}

Under the EU and UK General Data Protection Regulation (GDPR) and Turkey's Law on the Protection of Personal Data (KVKK, No. 6698), we rely on:

- **The contract with you**, for your account, monitors, alerts, status pages, API keys and connected apps.
- **Our legitimate interests**, for server logs, security, abuse prevention, the record of changes, and the fonts the site needs.
- **Your consent**, for Google Analytics. You can withdraw it at any time with Cookie settings.
- **Legal obligations**, when the law requires us to keep or disclose data.

## Your rights {#rights}

You can ask us for:

- a copy of your personal data, also in a machine-readable form;
- a correction of data that is wrong;
- the deletion of your account and data;
- a limit on, or an objection to, how we use your data;
- withdrawing your consent to analytics, which you can also do with Cookie settings.

Write to [info@imsu.io](mailto:info@imsu.io) from the email address of your account. We answer within 30 days. Much of your data you can see and change yourself in the web app: your monitors, alert channels, API keys and connected apps.

If you think we handle your data wrongly, you can complain to a data protection authority: in the EU, the one where you live or work; in Turkey, the Personal Data Protection Authority (KVKK).

## Security {#security}

Connections use HTTPS. Passwords, API keys, OAuth tokens and status page passwords are stored only as hashes. Monitors can only check addresses that are reachable from the public internet. Suspending or deleting an account, or removing someone from it, ends its API keys and connected apps at once.

## Children {#children}

imsu.io is not meant for anyone under 16, and we don't knowingly collect their data.

## Changes to this page {#changes}

When we change this page, we update the date at the top. If a change matters to how we use your data, we tell account owners by email before it applies.
