Heartbeat / cron monitors.
Inverted from the active checks: your job hits a URL on every successful
cycle. If we don't see a hit within intervalSec + heartbeatGraceSec,
an incident opens and your configured alert channels fire. The next
successful hit auto-resolves the incident.
TL;DR
Inverted from the active checks: your job hits a URL on every successful
cycle. If we don't see a hit within intervalSec + heartbeatGraceSec,
an incident opens and your configured alert channels fire. The next
...
Heartbeat / cron monitors.
Inverted from the active checks: your job hits a URL on every successful
cycle. If we don't see a hit within intervalSec + heartbeatGraceSec,
an incident opens and your configured alert channels fire. The next
successful hit auto-resolves the incident.
Receive URL
https://api.24observe.com/api/v1/heartbeats/<TOKEN>
Token is a 64-character lowercase hex string (32 random bytes). We generate it server-side when you create a heartbeat monitor — you never choose it.
Calling it
- Method:
POSTorHEAD(both work;HEADis convenient forcurl --head). - Body: none. We ignore any body you send.
- Auth header: none. The URL itself is the secret. Treat it like a password.
- Success:
204 No Content. Anything else = treat as failure on your side. - Rate limit: 60 / minute / IP. Plenty for any sane cron cadence.
# POST curl -fsS -X POST https://api.24observe.com/api/v1/heartbeats/<TOKEN> # HEAD (no body either way — both return 204) curl -fsS --head https://api.24observe.com/api/v1/heartbeats/<TOKEN>
Two-step token retrieval (programmatic flow)
Heartbeat tokens are encrypted at rest. POST /api/v1/monitors
with type: "heartbeat" creates the monitor and returns the row,
but the heartbeatToken field in that response is the
encrypted ciphertext envelope — not what you POST to. To get the
plaintext token your job will use, follow up with the role-gated
secrets read.
This requires an owner-or-admin PAT with the secrets:read
scope (or wildcard *). If you mint a tightly-scoped PAT for
this flow:
curl -X POST https://api.24observe.com/api/v1/me/tokens \
-H 'Authorization: Bearer obs_<admin>' \
-H 'Content-Type: application/json' \
-d '{
"name": "aeoniti-heartbeat-bootstrap",
"scopes": ["monitors:write", "secrets:read"]
}' Step 1 — create the monitor
curl -X POST https://api.24observe.com/api/v1/monitors \
-H 'Authorization: Bearer obs_...' \
-H 'Content-Type: application/json' \
-d '{
"name": "Aeoniti nightly job",
"url": "https://aeoniti.example/heartbeat-source",
"type": "heartbeat",
"intervalSec": 300,
"timeoutMs": 10000,
"heartbeatGraceSec": 60,
"alertEmail": "ops@aeoniti.example"
}'
# Response (201) — note the encrypted heartbeatToken; do not POST to it.
{
"id": 17,
"name": "Aeoniti nightly job",
"type": "heartbeat",
"intervalSec": 300,
"heartbeatToken": "{\"v\":2,\"kid\":\"default\",...}",
...
} Step 2 — fetch the plaintext token
curl https://api.24observe.com/api/v1/monitors/17/secrets \
-H 'Authorization: Bearer obs_<owner-or-admin>'
# Response (200)
{
"id": 17,
"heartbeatToken": "a1b2c3...<64 hex chars>",
...
}
# Build the receive URL
HEARTBEAT_URL="https://api.24observe.com/api/v1/heartbeats/a1b2c3..."
Every /secrets read is recorded in the audit log with
action: READ_MONITOR_SECRETS — useful when you want to
prove which integrations have ever fetched the token.
End-to-end agent example
TypeScript (Node 22)
const PAT = process.env.OBSERVE24_PAT!; // owner-or-admin
const BASE = 'https://api.24observe.com/api/v1';
async function provisionHeartbeat(name: string, intervalSec: 60 | 300 | 900) {
// 1. Create the monitor
const create = await fetch(`${BASE}/monitors`, {
method: 'POST',
headers: { authorization: `Bearer ${PAT}`, 'content-type': 'application/json' },
body: JSON.stringify({
name,
url: `internal://${name}`, // url is required by schema; not used for heartbeats
type: 'heartbeat',
intervalSec,
timeoutMs: 10000,
heartbeatGraceSec: 60,
alertEmail: process.env.OPS_EMAIL,
}),
});
if (!create.ok) throw new Error(`create: ${create.status} ${await create.text()}`);
const monitor = await create.json();
// 2. Fetch the plaintext heartbeat token
const sec = await fetch(`${BASE}/monitors/${monitor.id}/secrets`, {
headers: { authorization: `Bearer ${PAT}` },
});
if (!sec.ok) throw new Error(`secrets: ${sec.status}`);
const { heartbeatToken } = await sec.json();
return {
monitorId: monitor.id,
heartbeatUrl: `${BASE}/heartbeats/${heartbeatToken}`,
};
}
// Use in your job loop
async function ping(url: string) {
const r = await fetch(url, { method: 'POST' });
if (r.status !== 204) throw new Error(`heartbeat: ${r.status}`);
} Python
import os, httpx
PAT = os.environ['OBSERVE24_PAT'] # owner-or-admin
BASE = 'https://api.24observe.com/api/v1'
H = {'authorization': f'Bearer {PAT}', 'content-type': 'application/json'}
def provision_heartbeat(name: str, interval_sec: int) -> str:
create = httpx.post(f'{BASE}/monitors', headers=H, json={
'name': name,
'url': f'internal://{name}',
'type': 'heartbeat',
'intervalSec': interval_sec,
'timeoutMs': 10000,
'heartbeatGraceSec': 60,
'alertEmail': os.environ['OPS_EMAIL'],
})
create.raise_for_status()
monitor_id = create.json()['id']
sec = httpx.get(f'{BASE}/monitors/{monitor_id}/secrets',
headers={'authorization': f'Bearer {PAT}'})
sec.raise_for_status()
token = sec.json()['heartbeatToken']
return f'{BASE}/heartbeats/{token}'
# In your job loop
def ping(url: str):
r = httpx.post(url, timeout=10)
if r.status_code != 204:
raise RuntimeError(f'heartbeat: {r.status_code}') What happens when pings stop
- Last heartbeat was at
T. Monitor'sintervalSec+heartbeatGraceSectells us when it should have arrived by. - The scheduler ticks every 15 seconds; on the first tick after
T + intervalSec + heartbeatGraceSec, the monitor flips todown. - Once consecutive missed checks ≥
alertThreshold(default 1), an incident opens and the configured alert channels fire (email / webhook / Slack / Discord / Teams / Telegram). - The next successful POST automatically resolves the open incident — last_status flips back to
up,resolvedAtis set, recovery email goes out to status-page subscribers if that monitor is on a public page.
Tuning
intervalSec: how often you ping. One of 30, 60, 300, 900, 1800, 3600. Plan tier sets the floor.heartbeatGraceSec: slack window (default 60s). Set higher than your worst-case job duration so a slow run isn't a false miss.alertThreshold: how many consecutive misses before paging (default 1). Raise it if your job is flaky-but-recovers.
Security
- The token is the only auth — anyone with the URL can mark the monitor up. Don't log it, don't commit it, don't put it in a public-facing config.
- The DB stores only
sha256(token)for the lookup. A DB dump does not yield live tokens. - A leaked token? Open the monitor in the dashboard → Rotate. Old URL invalidates immediately and a new plaintext is shown once.
- Per-IP rate limit (60/min) makes brute-forcing the 64-hex space (1664) impractical even if an attacker guesses the URL prefix.
Troubleshooting
Heartbeat flapping or alerts not firing? See the runbook — sections "Heartbeat monitor flapping" and "Alerts aren't reaching me".
Confirming the alert channel works
Don't wait for a real outage to find out your channel is misconfigured. Open the monitor in the dashboard and use Send test alert to all configured channels — each channel reports per-channel pass/fail with the exact error.