Heartbeats — API v1 and MCP | Nodea Documentation
The Heartbeats module handles cron / dead-man-switch monitors: a service or cron job must periodically call the public ping URL (/hb/{token}), and a missed deadline (interval + grace) raises an alert. This document describes the REST API v1 endpoints and the module's MCP tools. The public ping endpoint is a separate, unauthenticated web route and is not part of this API.
Authentication, scopes and flag
Passport Bearer token (Authorization: Bearer <token>). Scopes: monitoring:read (index, show, runs), monitoring:write (create, update, pause, resume), monitoring:delete (delete). The whole module is gated by the features:heartbeats flag.
Ownership, visibility and token masking
Access is enforced by HeartbeatPolicy: view = owner or accepted FOREIGN share, manage = owner only. An existing but invisible id yields 403, an unknown id 404. Key security rule: the token and the derived ping_url are secrets (the customer's cron URL is derived from them), so they are serialized only for the owner (passing the manage policy). An accepted share sees the heartbeat but never its token. The token is generated once on create and is immutable — update never regenerates it.
API v1 — endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/heartbeats | monitoring:read | List heartbeats (owned + accepted shares), paginated. |
| GET | /api/v1/heartbeats/{heartbeat} | monitoring:read | Detail; meta.can_manage. Token for owner only. |
| GET | /api/v1/heartbeats/{heartbeat}/runs | monitoring:read | Ping/miss history, newest first. |
| POST | /api/v1/heartbeats | monitoring:write | Create a heartbeat (generates token). Returns 201. Plan limit → 402 (code plan_limit_exceeded). |
| PUT/PATCH | /api/v1/heartbeats/{heartbeat} | monitoring:write | Update name/interval/grace/note. Token immutable. |
| POST | /api/v1/heartbeats/{heartbeat}/pause | monitoring:write | Idempotent pause (sets paused_at). |
| POST | /api/v1/heartbeats/{heartbeat}/resume | monitoring:write | Idempotent resume (clears paused_at). |
| DELETE | /api/v1/heartbeats/{heartbeat} | monitoring:delete | Permanently delete (history cascade). Returns 204. |
GET /heartbeats — query parameters
search(string, max 255) — matched against name.status(enum:active|paused).sort(enum:name|interval_seconds|last_ping_at|created_at, defaultcreated_at).direction(asc|desc).per_page(int 1–100, default 20),page.
POST/PATCH — body parameters
name(string, required, max 255).intervalSeconds(int, required, 60–259200) — expected ping interval.graceSeconds(int, required, 0–86400) — grace period added on top of the interval.note(string, optional, max 5000).notificationGroupsId[](POST only) — must belong to the token user (otherwise422). Update does not accept group sync.
Response shape (HeartbeatResource)
Fields: id, name, status (paused→waiting→late→ok), interval_seconds, grace_seconds, note, token (owner only), ping_url (owner only), ownership (owner|shared), last_ping_at, deadline_at (only after the first ping), paused_at, created_at, updated_at. History entry (HeartbeatRunResource): id, heartbeat_id, state, state_name (Success = ping / Failure = missed deadline), reason, reason_code, created_at.
Error codes
401— missing/invalid token;403— scope/policy;404— unknown id;422— validation or a foreign notification group;402— plan limit exceeded on create (plan_limit_exceeded).
Examples — API
Create a heartbeat (every 5 min, 60s grace)
curl -s -X POST https://app.nodea.io/api/v1/heartbeats \
-H "Authorization: Bearer $NODEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly backup",
"intervalSeconds": 300,
"graceSeconds": 60,
"note": "cron 0 3 * * *"
}'
The response includes token and ping_url — the one-time moment to wire up the cron. The script then calls ping_url after each successful run.
List active heartbeats
curl -s "https://app.nodea.io/api/v1/heartbeats?status=active&sort=last_ping_at&direction=desc" \
-H "Authorization: Bearer $NODEA_TOKEN"
MCP — tools
Same auth/scope model as API v1 (ensureScope) and the features:heartbeats flag. The token and ping_url appear in the response only when the caller holds the manage ability (owner).
| Tool | Scope | Type | Description |
|---|---|---|---|
| heartbeats-list | monitoring:read | read-only | List; search, status (active/paused), page, per_page (max 50). No token. |
| heartbeat-get | monitoring:read | read-only | Detail; token + ping_url for owner only. Param: id. |
| heartbeat-create | monitoring:write | write | name, interval_seconds (60–259200), grace_seconds (0–86400), note. Plan limit → error. Returns token+ping_url. |
| heartbeat-update | monitoring:write | idempotent | Partial update of name/interval/grace/note. Owner only. Token immutable. |
| heartbeat-set-paused | monitoring:write | idempotent | Pause/resume via paused: true|false. |
| heartbeat-delete | monitoring:delete | destructive | Permanent delete; requires confirm: true. |
| heartbeat-runs | monitoring:read | read-only | Ping/miss history; page, per_page (max 50). |
Example — MCP (JSON-RPC)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "heartbeat-create",
"arguments": {
"name": "Nightly backup",
"interval_seconds": 300,
"grace_seconds": 60,
"note": "cron 0 3 * * *"
}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "heartbeat-set-paused",
"arguments": { "id": "3af0...uuid", "paused": false }
}
}