paulobrien.dev

Site Watch API

Site Watch API, webhooks and AI connector.

See what's up or down from your own code, add websites, post status page notices and get told the moment something stops working. Or ask Claude “is anything down?”. It all runs on your own Site Watch, in your own Cloudflare account.

1Get started

The API comes with Site Watch 1.2.0 and later. It lives on your own install:

https://watch.yourdomain.co.uk/api/v1
  1. Open the Account tabSign in to Site Watch and open Account. Your API address is under API, webhooks and AI connector, ready to copy.
  2. Make a keyGive it a name, tick It can change things if your script needs to add or pause monitors, and copy the key. It's shown once, and starts with watch_.
  3. Send it with each requestIn an Authorization header, like this:
curl https://watch.yourdomain.co.uk/api/v1/monitors \
  -H "Authorization: Bearer watch_••••••••••••••••••••••••••••••••"

2Keys and permissions

Make a separate key for each script or app, so you can delete one without stopping the others. You can have up to 10.

KeyWhat it allows
Read onlyEverything you watch, uptime, outages, alerts and status pages.
Read and changeAll of that, plus adding, checking, pausing and removing monitors, and posting and resolving status page notices.
The same rules as the app. Changes made with a key go through the same code as the app's own buttons, so the same limits and checks apply.

3Every request

RequestNeedsWhat it does
GET /monitorsReadEverything you watch, with status, uptime over 30 and 90 days, response time, certificate expiry and domain renewal.
GET /monitors/{id}ReadOne monitor, with its last 10 outages.
POST /monitorsChangeStart watching something. kind is website, mail, port or heartbeat. See the example below.
POST /monitors/{id}/checkChangeCheck it now.
POST /monitors/{id}/pauseChangePause it, for example {"minutes": 60} during maintenance.
POST /monitors/{id}/resumeChangeStart checking again.
DELETE /monitors/{id}ChangeStop watching it.
GET /alertsReadThe latest alerts, newest first. Takes limit (up to 100).
GET /pagesReadYour status pages, which monitors each shows, and notices from the last 14 days.
POST /pages/{id}/noticesChangePost a notice: {"title": "…", "body": "…", "impact": "minor"}. Impact is minor, major or maintenance.
POST /notices/{id}/resolveChangeMark a notice resolved.

A monitor's status is up, down, cert (a certificate problem), blocked (the server turned the checks away), unsure (one failed check), paused or checking.

4Examples

Is anything down?

curl https://watch.yourdomain.co.uk/api/v1/monitors \
  -H "Authorization: Bearer $WATCH_KEY"

The answer (shortened)

{
  "monitors": [
    {
      "id": "3f9a1c2b7d4e8a60",
      "kind": "website",
      "label": "yourdomain.co.uk",
      "url": "https://yourdomain.co.uk/",
      "status": "up",
      "responseMs": 182,
      "uptime": { "30d": 99.98, "90d": 99.95 },
      "checkedAt": "2026-11-04T17:00:00Z",
      "certificate": { "expires": "2027-01-20T23:59:59Z", "daysLeft": 77, "renewsItself": true },
      "domain": { "name": "yourdomain.co.uk", "expires": "2027-06-01T00:00:00Z", "daysLeft": 209 }
    }
  ]
}

Start watching a website

curl https://watch.yourdomain.co.uk/api/v1/monitors \
  -H "Authorization: Bearer $WATCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "website", "url": "https://shop.yourdomain.co.uk/", "every": 5 }'

every is how often to check, in minutes: 1, 5, 10, 15, 30 or 60. A mail server or port takes host and port instead of url. A heartbeat takes a name and how often the job runs, and the answer gives you its pingUrl.

Pause during maintenance

curl -X POST https://watch.yourdomain.co.uk/api/v1/monitors/3f9a1c2b7d4e8a60/pause \
  -H "Authorization: Bearer $WATCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "minutes": 30 }'

In JavaScript

const res = await fetch(`${WATCH_API_URL}/monitors`, {
  headers: { Authorization: `Bearer ${process.env.WATCH_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).error);
const { monitors } = await res.json();
const down = monitors.filter((m) => m.status === "down");

5Webhooks

Add Your own webhook on the Alerts tab, with its https:// address. Site Watch posts to it every time it sends an alert.

EventWhen it's sent
monitor.downSomething stopped working.
monitor.upIt's working again.
monitor.flappingIt keeps going down and coming back.
monitor.blockedThe server is turning the checks away.
monitor.certThe website's certificate isn't valid.
cert.expiringThe certificate runs out soon and doesn't renew itself.
domain.expiringThe domain's registration runs out soon.
alert.testThe Send a test button.

What Site Watch sends

{
  "id": "evt_8c1d2e3f4a5b6c7d8e9f0a1b",
  "type": "monitor.down",
  "created": "2026-11-04T17:02:11Z",
  "app": "Site Watch",
  "monitor": { "id": "3f9a1c2b7d4e8a60", "kind": "website", "label": "yourdomain.co.uk", "url": "https://yourdomain.co.uk/", "host": null, "port": null },
  "subject": "yourdomain.co.uk is down",
  "text": "yourdomain.co.uk stopped answering at 17:02 UTC. …",
  "urgent": true,
  "url": "https://watch.yourdomain.co.uk/"
}

Each request also carries Site-Watch-Event, Site-Watch-Delivery (an id for this post) and Site-Watch-Signature. Answer with any 2xx status within 10 seconds. Site Watch also sends to Slack, Teams, Discord, Google Chat and email, so you only need a webhook for your own code.

Checking a webhook is genuine

Each webhook has its own signing secret (starting whsec_), shown when you add it. The app signs <t>.<body> with HMAC-SHA256 using that secret, and sends the result in the Site-Watch-Signature header as t=1760520601,v1=5f2b…. To check a request, work out the same signature from the raw body, compare it with v1 and refuse anything more than five minutes old.

Node.js

import crypto from "node:crypto";

function isFromSiteWatch(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.v1 || age > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Python

import hashlib, hmac, time

def is_from_site_watch(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if "v1" not in parts or abs(time.time() - int(parts["t"])) > 300:
        return False
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Use the body exactly as it arrived. Turning the JSON into an object and back into text can change it, and then the signature won't match.

6AI connector

Add Site Watch to Claude, ChatGPT or any MCP client. In Claude, open Settings › Connectors › Add custom connector and paste:

https://watch.yourdomain.co.uk/mcp

Claude asks you to sign in to Site Watch once. Then ask things like “is anything down?” or “what was my uptime last month?”. It can only read. Disconnect AI apps on the Account tab signs it out. Apps that let you add a header can send a watch_ key instead of signing in.

7Errors and limits

Anything that goes wrong comes back with a status, a sentence you can show to a person and a link to this page:

{"error": "This key can only read. Make a key that can change things on the Account tab.", "docs": "https://paulobrien.dev/watch/api/"}
StatusWhat it means
400Something in the request needs fixing, such as a key put in the web address.
401No key, or a key that has been deleted.
403The key can only read.
404Not found, or a request the API doesn't have.
405Use GET to read, POST to make or change and DELETE to remove.
429Each key can make 60 requests a minute. Retry-After says how long to wait.

Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining. There are no browser (CORS) headers on purpose: keep keys on your server, or in tools like Zapier, Make or n8n.

Questions or ideas? Ask in GitHub Discussions, or open an issue for a bug.

Stay in the loop

Subscribe for updates.

Leave your email and I'll tell you when there's a new version of one of my apps, or a new project to try. Nothing else.

That didn't work. Please check your email address and try again.

One email to confirm. Unsubscribe with one click. Your address is only used for these updates.