Heartbeat API · v1
Know when your jobs go quiet.
Create a monitor, send it a small HTTPS request on each successful run, and receive a state-change event when it misses its deadline.
https://dead-mans-heartbeat.pages.devQuick start
Set HEARTBEAT_ADMIN_KEY, start the service, then use that value as your administrator Bearer token. Create calls return a heartbeat token exactly once; store it in your job’s secret manager.
curl -X POST https://dead-mans-heartbeat.pages.dev/v1/monitors \
-H "Authorization: Bearer $HEARTBEAT_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Nightly database backup","interval_sec":86400,"grace_period_sec":300}'
# Response includes heartbeat_token and heartbeat_path
curl -X POST https://dead-mans-heartbeat.pages.dev/v1/ping/mon_abc123 \
-H "Authorization: Bearer hb_live_…"Authentication
Management endpoints require Authorization: Bearer <API key>. The environment bootstrap key is suitable for initial local setup; create operational keys through /v1/api-keys afterward. Stored API keys and heartbeat tokens are SHA-256 hashes, never plaintext.
Heartbeat ingestion uses a separate token generated when the monitor is created. Do not use an administrator API key for pings.
Send a heartbeat
/v1/ping/:monitor_idA successful heartbeat sets the monitor to up, saves its received timestamp, and writes one basic history record. If the previous status was down, a recovery event is sent asynchronously.
Optional JSON body
{
"status": "success",
"execution_time_ms": 240,
"metadata": { "node": "db-backup-job-01" }
}execution_time_msmust be an integer from 0 to 86,400,000.metadatamust be an object no larger than 4 KB.- Requests larger than 8 KB are rejected.
Monitors
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/monitors | List your monitors. |
| POST | /v1/monitors | Create and receive its heartbeat token once. |
| GET | /v1/monitors/:id | Get one monitor. |
| PATCH | /v1/monitors/:id | Update name, interval, or grace period. |
| DELETE | /v1/monitors/:id | Delete a monitor and its history. |
Create payload
{ "name": "Nightly database backup", "interval_sec": 86400, "grace_period_sec": 300 }interval_sec is 300 to 31,536,000 seconds; grace_period_sec is 0 to 86,400 seconds. The free MVP plan permits three monitors and a five-minute minimum interval.
History
/v1/monitors/:id/logs?limit=50Returns newest-first heartbeat records. limit defaults to 50 and is capped at 200.
Build your own dashboard
You can build a status page, admin panel, or embedded widget without using the Heartbeat dashboard. When you create a monitor, save the returned public_status_token and call this read-only endpoint from your frontend:
/v1/public/monitors/:idconst response = await fetch(
'https://dead-mans-heartbeat.pages.dev/v1/public/monitors/mon_abc123',
{ headers: { 'x-public-status-token': import.meta.env.VITE_HEARTBEAT_STATUS_TOKEN } }
});
const result = await response.json();
// result.monitor.status: "up" | "down"
This token exposes only that monitor’s status and timing fields. Keep the heartbeat token and management API keys on your server. The endpoint supports cross-origin browser requests and returns 401 for an invalid token.
API keys
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/api-keys | List keys without their token value. |
| POST | /v1/api-keys | Create a key; the token is returned once. |
| DELETE | /v1/api-keys/:id | Immediately revoke a key. |
Webhooks
Webhooks are delivered only for state changes: monitor.down and monitor.recovered. Delivery is best-effort and does not delay the ping response.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/webhooks | List webhooks. |
| POST | /v1/webhooks | Create one and receive its signing key once. |
| PATCH | /v1/webhooks/:id | Change the URL or enabled state. |
| DELETE | /v1/webhooks/:id | Delete it. |
Each delivery carries x-heartbeat-signature: sha256=<hex>, an HMAC SHA-256 over the raw JSON body using the returned signing key. Private hosts and loopback addresses are refused.
Run the scheduler every minute
The application deliberately keeps scheduling outside the request process. Use your host’s cron, Cloudflare Cron Trigger, or another trusted scheduler to call the protected endpoint every minute. It changes a monitor from UP to DOWN exactly once after last_ping_at + interval_sec + grace_period_sec.
/v1/internal/check-timeouts* * * * * curl --fail -X POST https://dead-mans-heartbeat.pages.dev/v1/internal/check-timeouts \
-H "x-scheduler-secret: $HEARTBEAT_SCHEDULER_SECRET"Runtime examples
# Python
import requests
requests.post("https://dead-mans-heartbeat.pages.dev/v1/ping/mon_abc123", headers={"Authorization": "Bearer hb_live_…"})
// Node.js
await fetch("https://dead-mans-heartbeat.pages.dev/v1/ping/mon_abc123", { method: "POST", headers: { Authorization: "Bearer hb_live_…" } });
// ESP32: use WiFiClientSecure + HTTPClient and POST the same URL and Bearer header.
For cron, run the curl command above once per minute (or at your monitor’s interval). Keep tokens in environment variables or a secret manager.
Responses and status codes
200 { "success": true, ... }
201 { "success": true, "monitor": { ... }, "heartbeat_token": "..." }
400 { "success": false, "error": { "message": "..." } }
401 Missing, invalid, or revoked credentials
403 Free-plan monitor limit reached
404 Resource does not exist or belongs to another account
413 Payload exceeds the 8 KB heartbeat limit
429 Rate limit exceededMonitor objects contain id, userId, name, status, intervalSec, gracePeriodSec, lastPingAt, createdAt, and updatedAt. Create endpoints return secrets once; they are not available from later GET calls.
State transitions
up → down occurs once when the scheduler passes the deadline. down → down emits nothing. The next valid heartbeat changes down → up and emits a recovery event. Normal up → up heartbeats are silent. This prevents alert storms.
Deployment and configuration
Run bun run deploy to build, apply pending D1 migrations, and upload the Cloudflare Pages bundle. The existing D1 database is bound as DB. Upload secrets with bun run deploy:secrets (or add them in Pages → Settings → Environment variables): HEARTBEAT_ADMIN_KEY, HEARTBEAT_SCHEDULER_SECRET, and optional TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, RESEND_API_KEY, ALERT_EMAIL_TO, and ALERT_EMAIL_FROM. Schedule POST /v1/internal/check-timeouts from an external cron service every minute.
Accounts and dashboard
Use POST /v1/auth/signup with { "name": "Ada", "email": "ada@example.com", "password": "at-least-8-chars" }, then POST /v1/auth/login. Both set an HttpOnly session cookie. The dashboard is available at /dashboard; it lists monitors and lets you create one. API clients may continue using Bearer API keys.
Plans and limits
| Plan | Monitors | History | Notifications |
|---|---|---|---|
| Free | 3 | 7 days (retention policy to be added) | Email, Telegram |
| Pro · $7/month | 50 | 90 days (retention policy to be added) | Email, Telegram, Webhooks |
The current service enforces the free three-monitor limit and five-minute minimum interval. Billing and automatic plan upgrades are not included.
Rate limiting
Heartbeat ingestion allows 120 requests per source IP per minute in the application process and returns 429 when exceeded. Production deployments should add an edge or gateway limiter (Cloudflare Rate Limiting, for example) because in-memory counters reset on restart and are local to each instance.
Errors and health
Errors use a stable envelope: { "success": false, "error": { "message": "…" } }. Use GET /v1/health for a simple uptime probe. Management endpoints return 401 for missing, invalid, or revoked credentials; resources outside the caller’s ownership return 404.