~3 min readsynced 2026-05-29·/api/v1 + ping routes@live
Your first heartbeat
PingWatch is a dead-man's switch: you tell it how often to expect a beat, then have your job curlits ping URL when it finishes. As long as the beats keep arriving on time, PingWatch stays quiet. When one doesn't arrive inside the window, it pages you. Add a single line to the endof your cron or backup job and you're covered.
~/projects/acme · zsh
$# at the very END of your backup scriptcurl-fsS"https://pingwatch.dev/api/ping/d290f1ee-6c54-4b01-90e6-d701748f0851"
PingWatch listens for a periodic "I'm alive" signal from your jobs and pages your on-call when the signal stops arriving.
What it does
Receive heartbeats over HTTP / one-line shell snippet
Alert through email and webhook
Flexible windows: "expect a beat every 5m, ±30s tolerance"
What it does not do
↛ Not usActively probe your URL
PingWatch is passive. WatchTower probes.
→ try WatchTower
↛ Not usSchedule the work itself
PingWatch knows you're late, not how to fire on time.
→ try CronPilot
↛ Not usPublic outage page
PingWatch is internal alerting.
→ try FlipSign
Create a monitor
A monitor is one heartbeat expectation. Create it in the dashboard (New monitor) or over the API. The only required fields are namerequired and schedulerequired — an interval string like 5m, 1h, or 24h. The response carries the monitor's id, which is the heart of its ping URL.
curl
curl-XPOST"https://pingwatch.dev/api/v1/monitors"\-H"Authorization: Bearer <PINGWATCH_API_KEY>"\-H"Content-Type: application/json"\-d{"name":"Nightly DB backup","schedule":"24h","gracePeriodSeconds":600}'
Response · 201
{"id":"d290f1ee-6c54-4b01-90e6-d701748f0851","userId":"8RXc1Pm9-...","name":"Nightly DB backup","slug":null,"schedule":"24h","scheduleType":"interval","gracePeriodSeconds":600,"status":"new","lastPingAt":null,"lastPingType":null,"nextExpectedAt":null,"alertSentAt":null,"consecutiveMisses":0,"isPaused":false,"createdAt":"2026-05-29T17:55:00.000Z","updatedAt":"2026-05-29T17:55:00.000Z"}
A fresh monitor starts in status newand won't alert until it has received its first ping. The gracePeriodSeconds (default 300, range 60–86400) is the slack you allow past the interval before the monitor is considered late.
Ping at the end of the job
Drop the ping at the very end of the work, after it has actually succeeded. A bare GET or POST to the ping URL records a success: the monitor flips to healthy, the stale timer resets, and any active alert is cleared. Use curl -fsS so the ping itself never silently fails.
backup.sh
#!/usr/bin/env bashset-euopipefailPING="https://pingwatch.dev/api/ping/d290f1ee-6c54-4b01-90e6-d701748f0851"# 1. tell PingWatch the job has started (optional — enables duration timing)curl-fsS"$PING/start">/dev/null# 2. do the workpg_dumpmydb|gzip|awss3cp-s3://backups/mydb.sql.gz# 3. signal success — resets the timer and clears any active alertcurl-fsS"$PING">/dev/null
Start, success & fail
Three ping endpoints share the same monitor, addressed by {id} (UUID or custom slug):
POST /api/ping/{id}/start — fire when the job begins. PingWatch records the start so a later success can be timed (start → success duration), but it does not reset the stale timer on its own.
GET|POST /api/ping/{id} — the success beat. This is the one that keeps the monitor healthy.
GET|POST /api/ping/{id}/fail — report an explicit failure. The monitor goes down immediately and your on-call is paged, rather than waiting for the window to lapse.
backup.sh — report failures
# wrap the job so a non-zero exit reports a failure immediatelyPING="https://pingwatch.dev/api/ping/d290f1ee-6c54-4b01-90e6-d701748f0851"ifpg_dumpmydb|gzip|awss3cp-s3://backups/mydb.sql.gz;thencurl-fsS"$PING"elsecurl-fsS"$PING/fail"#marksthemonitor"down"andpagesyouron-callfi
Custom slugs
The UUID works everywhere, but you can give a monitor a readable slug and ping /api/ping/nightly-backup instead. Set it on create or update with the slug field (or from the dashboard). Slugs are lowercase letters, numbers, and single dashes, 2–64 characters.
Every monitor exposes a live SVG badge at /api/badge/{id} (UUID or slug — no auth needed). Drop it in a README or status page; it renders the monitor name and its current state (healthy, late, down, paused, or new).