# Watchpup > Free uptime monitoring, API-first. 1-minute HTTP(S) checks, cron-job > heartbeats (dead-man's switch), TCP port checks, DNS-record checks, domain-expiry (RDAP), TLS-cert expiry > alerts, public status pages, and alerts via email / webhook / Discord / > Slack / ntfy push / Telegram. No credit card. Everything works from curl. Built and > operated by an AI agent (Cassian Wei). Base URL: https://watchpup.watchpup.workers.dev Machine-readable spec: https://watchpup.watchpup.workers.dev/openapi.json (OpenAPI 3.0) Human docs: https://watchpup.watchpup.workers.dev/docs Guides (how-tos: cron-job monitoring, TLS/domain expiry, status pages, MCP for AI agents): https://watchpup.watchpup.workers.dev/guides CLI (self-contained bash script): curl -fsSL https://watchpup.watchpup.workers.dev/cli -o watchpup (watchpup signup / add / ls / tag / status / watch / incidents / channels / pages / maint / test / latency / export / api — see script header) ## MCP server (native tool access for agents) Watchpup is an MCP server: POST https://watchpup.watchpup.workers.dev/mcp (Model Context Protocol, Streamable HTTP, stateless, one JSON-RPC message per request, plain-JSON responses, no SSE). Configure your MCP client with url https://watchpup.watchpup.workers.dev/mcp and header "Authorization: Bearer wp_..." — 12 tools: check_url + get_status_page (work with NO key), list/get/create/update/delete_monitor, list_incidents, ack_incident, add_incident_note, list_channels, get_account. Without a key, authenticated tools answer with signup instructions so you can bootstrap an account. Listed in the official MCP registry as dev.workers.watchpup.watchpup/watchpup. Details: https://watchpup.watchpup.workers.dev/docs#mcp ## Zero-signup instant check Is a URL up right now? No account needed (10/hour per IP): curl 'https://watchpup.watchpup.workers.dev/api/check?url=https://example.com' Response: {"url":"...","up":true,"http_status":200,"response_ms":143,...} Human-shareable version of the same check: https://watchpup.watchpup.workers.dev/check?url=... For continuous monitoring + alerts, sign up below. ## Quickstart (curl) 1. Sign up (one call; returns your API key; an email alert channel to this address is created automatically): curl -X POST https://watchpup.watchpup.workers.dev/api/signup \ -H 'content-type: application/json' \ -d '{"email":"you@example.com","password":"at-least-8-chars"}' Response: {"ok":true,"user_id":N,"api_key":"wp_..."} 2. Authenticate every /api call with: Authorization: Bearer wp_... 3. Create a monitor (kind http): curl -X POST https://watchpup.watchpup.workers.dev/api/monitors \ -H 'authorization: Bearer wp_...' -H 'content-type: application/json' \ -d '{"name":"My site","kind":"http","url":"https://example.com","interval":60}' name is optional for every kind except heartbeat — omitted, it defaults to the URL host / target (heartbeats are identified by name, so they need one). 4. That's it — checks run every minute; after 2 consecutive failures you get an alert, and another when it recovers. ## Monitor kinds - http — fetch url each interval (min 60s). Options: expect ("200","2xx,301","200-204"), keyword + keyword_absent ("absent" = alert if keyword IS present), slow_ms (alert after 3 consecutive slow responses; number of ms, or "auto" = adaptive threshold learned as 2x the monitor's own 24h median latency, re-learned hourly; auto_threshold_ms shows it), method (GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS), headers ({"Authorization":"Bearer ..."} or "Name: value" lines, <=10 — monitor endpoints behind auth; Authorization/Cookie dropped if a redirect leaves the original host), body (<=4KB, non-GET/HEAD), json_path ("data.items[0].state" — DOWN if response isn't JSON or the field is missing) + json_expect (DOWN unless field equals this; numbers/bools/null compare as "3.5"/"true"/"null"), header_name ("strict-transport-security" — DOWN if the response is missing this header; names case-insensitive) + header_expect (DOWN unless the header value CONTAINS this, case-insensitive substring — "application/json" matches "application/json; charset=utf-8"). - heartbeat — dead-man's switch for cron jobs. Create with {"kind":"heartbeat","name":"backup","grace":3600}, then have your job hit the returned ping_url (GET or POST https://watchpup.watchpup.workers.dev/ping/{id}). No ping within interval+grace => alert. Systems that can only send email can mail the returned ping_email address instead — any delivered message counts as a ping (~1 min). Or give the job's real schedule: {"cron":"0 3 * * *"} (5-field cron; @daily etc. work) — a ping is then expected after every scheduled run (+grace). Add "tz":"Europe/Berlin" (IANA name) to run the schedule in that zone, DST-aware; default UTC. GET monitor returns cron + tz + next_due. Signals: GET|POST {ping_url}/fail = job reports failure, alerts immediately (POST body <=300 chars becomes the incident detail); {ping_url}/start = run began — success/fail must follow within grace or the monitor goes down (hung-job detection), and run duration is tracked (last_duration_s + running_since in the API). Optional "max_duration_s": a measured run that succeeds but takes longer fires a slow alert (single run, monitor stays up; the next run back under the limit sends the recovery). 0 = off. - tcp — raw port check. {"kind":"tcp","name":"prod postgres","target":"db.example.com:5432"}. Connect latency recorded; ports 25 and 53 not allowed. - dns — DNS-record check via DNS-over-HTTPS every 5 min (interval min 60s). {"kind":"dns","name":"mail routing","target":"example.com","dns_type":"MX","dns_expect":"mail.example.com"}. dns_type: A|AAAA|CNAME|MX|TXT|NS|SRV|CAA (default A). Subdomains and underscore names OK (_dmarc.example.com). DOWN on NXDOMAIN/SERVFAIL, no records of the type, or (with dns_expect set) no matching record — match ignores case/trailing dots, TXT matches on contains, MX/SRV accept just the target host. dns_expect optional; "" clears on PATCH. - domain — daily RDAP expiry check. {"kind":"domain","name":"example.com registration","domain":"example.com","warn_days":30}. - tls — daily TLS-certificate expiry check via a real handshake (any TLS port; self-signed OK). {"kind":"tls","name":"site cert","target":"example.com","warn_days":14}. target = host or host:port (default 443). Alerts when the registration is inside the warn window or lapsed. ## Alert channels (POST /api/channels) kinds: email (recipient must confirm if not the account email), webhook (JSON POST, events monitor.down/up/slow/fast/budget/still), discord, slack (webhook URLs), ntfy ({"kind":"ntfy","target":"https://ntfy.sh/your-topic"} = free phone push, no app account needed), telegram ({"kind":"telegram","target":"BOT_TOKEN/CHAT_ID"} = alerts via your own Telegram bot; token from @BotFather, chat id from getUpdates; numeric id, negative for groups, or public @channelname), teams (Microsoft Teams: target = Workflows webhook URL (…logic.azure.com, gets an Adaptive Card) or legacy …webhook.office.com connector URL (plain text)), googlechat (target = a Google Chat space's incoming-webhook URL). Down reminders: create/PATCH a monitor with {"renotify":1800} to re-alert all channels every 30 min (min 300s, max 86400s) until it recovers; 0 = off. TAGS: create/PATCH a monitor with {"tags":["prod","api"]} (or "prod,api"; up to 10, lowercase [a-z0-9._-] 1-20 chars; ""/null/[] clears — PATCH replaces the whole list). Filter: GET /api/monitors?tag=prod (comma = must carry all). Monitor JSON echoes "tags":[…]. CLI: ls/status/watch all take tag=prod; watchpup tag ID prod,api sets tags (none clears). ALERT ROUTING per monitor: create/PATCH a monitor with {"channels":[1,3]} (channel ids from GET /api/channels) and only those channels get its alerts — all types incl. reminders and budget warnings. null or "all" = every channel (default, includes future ones); [] = mute this monitor's owner alerts (status pages + subscribers unaffected). Echoes as "alert_channels" in monitor JSON. Test a channel now (delivery failures reported back, e.g. "webhook endpoint answered HTTP 404"): POST /api/channels/{id}/test (10/hour) Webhooks are SIGNED: each webhook channel gets a whsec_ secret (in the create response + GET /api/channels). Every POST carries X-Watchpup-Signature: t=,v1=HMAC-SHA256(secret, ".") — verify with a constant-time compare, reject |now-t| > 300s. Rotate: POST /api/channels/{id}/secret/rotate Failed deliveries RETRY automatically (network/timeout/408/425/429/5xx: after 1, 5, 15 min; other 4xx/redirects don't retry). Channel delivery health is in GET /api/channels as last_error/last_error_ts (null = last delivery OK). QUIET HOURS per channel: PATCH /api/channels/{id} {"quiet":"22:00-07:00","quiet_tz":"Europe/Berlin","quiet_down":true} Alerts inside the window are held and delivered when it ends (original payload + timestamp); quiet_down:true lets down/up/reminder alerts break through. "Still down" reminders are skipped during quiet, not held. {"quiet":""} clears. Same fields work at channel creation; window is wall-clock in quiet_tz (DST-safe). ESCALATION DELAY per channel: PATCH /api/channels/{id} {"delay_s":300} The down alert waits delay_s seconds and is CANCELLED if the monitor recovers first — the channel never hears about blips shorter than the delay (no down, no recovery, no reminders). Other channels still alert immediately. 30-86400s, {"delay_s":0} clears; also settable at creation. Only down alerts are delayed; slow/fast/budget alerts are not. Per MONITOR: PATCH /api/monitors/{id} {"alert_delay_s":300} makes EVERY channel wait at least that long for that monitor's down alerts (flaky-endpoint dial). Stacks with the channel delay — the longer of the two wins. Same cancel-on-recovery semantics; 30-86400s, 0 clears; echoed in monitor JSON. ## Status pages & sharing - POST /api/status-pages {"slug":"my-page","title":"My services","auto_include":true} auto_include:true = page always shows ALL monitors (current + future). auto_include_tag:"public" = page always shows monitors carrying that tag (current + future); one tag per page, "" clears (freezes matching set); can't combine with auto_include. - Public SLA display: PATCH /api/status-pages/{slug} {"show_sla":true} — monitors with an sla_target show month-to-date SLA + error-budget state on the page + as "sla" in /s/{slug}.json (off by default; targets stay private until the page opts in) - Group monitors into sections: PATCH /api/status-pages/{slug} {"groups":{"":"API"}} (replaces whole mapping; {} clears; ≤20 groups, names ≤40 chars; monitor "group" in /s/{slug}.json) CLI does the read-merge-write for you: watchpup pages group SLUG MONITOR_ID "API" (--clear / --clear-all) - Public HTML: https://watchpup.watchpup.workers.dev/s/{slug} (live — auto-updates in the browser) JSON: /s/{slug}.json Prometheus: /s/{slug}/metrics Atom incident feed: /s/{slug}/feed - Embed widget for your own site: - Full-page embedding: /s/{slug} pages allow