Heartbeat API · v1 · MVP documentation
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.
Custom dashboard guide
Use the API to build a status page inside your own product. Your frontend only needs a monitor ID and its read-only public token.
1. Create a monitor
Run this from your backend or terminal with an API key. The response returns both secrets once.
curl -X POST https://dead-mans-heartbeat.pages.dev/v1/monitors \
-H "Authorization: Bearer $HEARTBEAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Payments worker","interval_sec":300}'Save monitor.id and public_status_token. Use the heartbeat token only in the job that sends pings.
2. Fetch status from your UI
/v1/public/monitors/:idconst monitorId = 'mon_abc123';
const statusToken = 'status_…';
const response = await fetch(
`${baseUrl}/v1/public/monitors/${monitorId}`,
{ headers: { 'x-public-status-token': statusToken } }
});
const result = await response.json();Python client
import requests
response = requests.get(
"https://dead-mans-heartbeat.pages.dev/v1/public/monitors/mon_abc123",
headers={"x-public-status-token": "status_…"},
timeout=10,
)
monitor = response.json()["monitor"]
print(monitor["status"])The endpoint is CORS-enabled, so it can be called directly by a browser dashboard. You can also send the token as ?token=status_….
3. Render the state
const state = result.monitor.status;
const label = state === 'up' ? 'All systems operational' : 'Action needed';
document.querySelector('#status').textContent = label;Poll every 30–60 seconds, or refresh when your app route loads. The response includes status, lastPingAt, intervalSec, gracePeriodSec, and updatedAt.
Response shape
{
"success": true,
"monitor": {
"id": "mon_abc123",
"name": "Payments worker",
"status": "up",
"intervalSec": 300,
"gracePeriodSec": 60,
"lastPingAt": "2026-09-17T06:30:00.000Z",
"updatedAt": "2026-09-17T06:30:00.000Z"
}
}Keep public status tokens in frontend environment configuration, but never put heartbeat tokens, management API keys, or scheduler secrets in browser code.
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": "...", "public_status_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.