heartbeatAPI v1

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.

Base URLhttps://dead-mans-heartbeat.pages.dev

Quick 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

POST/v1/ping/:monitor_id

A 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_ms must be an integer from 0 to 86,400,000.
  • metadata must be an object no larger than 4 KB.
  • Requests larger than 8 KB are rejected.

Monitors

MethodPathPurpose
GET/v1/monitorsList your monitors.
POST/v1/monitorsCreate and receive its heartbeat token once.
GET/v1/monitors/:idGet one monitor.
PATCH/v1/monitors/:idUpdate name, interval, or grace period.
DELETE/v1/monitors/:idDelete 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

GET/v1/monitors/:id/logs?limit=50

Returns 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

GET/v1/public/monitors/:id
const 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

MethodPathPurpose
GET/v1/api-keysList keys without their token value.
POST/v1/api-keysCreate a key; the token is returned once.
DELETE/v1/api-keys/:idImmediately 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.

MethodPathPurpose
GET/v1/webhooksList webhooks.
POST/v1/webhooksCreate one and receive its signing key once.
PATCH/v1/webhooks/:idChange the URL or enabled state.
DELETE/v1/webhooks/:idDelete 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.

POST/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 exceeded

Monitor 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

PlanMonitorsHistoryNotifications
Free37 days (retention policy to be added)Email, Telegram
Pro · $7/month5090 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.