Everything you need to wire your first cron job in under five minutes. The HTTP API is intentionally tiny — one ping URL with three lifecycle actions (and a heartbeat shorthand) — so you can integrate from any language without an SDK. Node, PHP and WordPress get first-class wrappers either way: the cronheart npm package (six scheduler adapters plus a CLI that wraps any command), the cron-monitor/php-sdk package (Symfony Scheduler + Laravel), and the Cronheart WordPress plugin (WP-Cron).
Prefer to set things up programmatically? Beyond this ping endpoint there's a token-authenticated REST API to create and list monitors and channels — see the REST API reference.
Concepts
A monitor is one scheduled job you want to watch. Every monitor has a UUID — that UUID is also the secret in its ping URL. Treat it as a credential.
Monitors transition through four states:
Ping API
All endpoints share one ping URL, accept GET, POST, and HEAD, and
are idempotent within a 2-second window — a curl --retry
that briefly stumbles over a transient network blip won't
double-count. POST lets you attach up to 10KB of captured
output (stdout/stderr) — anything past 10KB is truncated, not
rejected.
POST /ping/<uuid> — heartbeat
The simplest case. Tells us "the job ran successfully right now".
Equivalent to POST /ping/<uuid>/success.
curl -fsS -m 10 --retry 5 https://cronheart.com/ping/<uuid>
POST /ping/<uuid>/start — run started
Send before the job runs. Lets us track duration and surface "job
stuck" warnings if a /start isn't followed by a
terminal event within the grace period.
POST /ping/<uuid>/success — run finished cleanly
Send when the job exits 0. Pair with /start to get a
duration column on the dashboard.
POST /ping/<uuid>/fail — run errored
Send when the job exits non-zero. Opens an incident and alerts immediately if one isn't already open — no need to wait for the next scheduled window.
Bash recipe with start + result reporting
UUID=<your-monitor-uuid>
BASE=https://cronheart.com/ping/$UUID
curl -fsS -m 10 --retry 5 "$BASE/start"
if /usr/bin/php /app/bin/console hourly:rollup; then
curl -fsS -m 10 --retry 5 --data-binary @<(tail -c 8000 ./run.log) "$BASE/success"
else
curl -fsS -m 10 --retry 5 --data-binary @<(tail -c 8000 ./run.log) "$BASE/fail"
fi
Schedule formats
Three formats; the form's preview shows the next 5 runs for all of them as you type — sanity-check before saving.
- Cron — standard 5-field expression
(
*/5 * * * *), interpreted in the monitor's timezone. - Interval — integer seconds between
runs:
300= 5 min,3600= 1 h. Range 30 s to 366 days. Timezone-agnostic. - Simple — one of a fixed allowlist:
every_minute,every_5_minutes,every_10_minutes,every_15_minutes,every_30_minutes,hourly,every_2_hours,every_6_hours,daily(00:00),daily_morning(09:00),weekly(Mon 00:00),monthly(1st 00:00). For arbitrary times use Cron.
Alerts & channels
An alert fires the moment a monitor goes late or
down with no incident already open — no fixed-window
batching, so the time-to-page is "the scanner sweep that detected
it" (≤ 30 s) plus the channel transport latency. That alert opens
an incident, and only the next successful run — a
/success or heartbeat ping — closes it with a recovery
alert. Pausing, resuming, snoozing and a /start ping
leave it open, and another failure or missed run while it's open
doesn't page you again.
Channels are configured once per account and shared across all your projects. Currently supported:
- Email (default)
- Telegram bot (set up via
/startwith the bot) - Slack incoming webhook
- Discord incoming webhook
- Generic webhook with HMAC-SHA256 signature
Email alerts ship a one-click List-Unsubscribe header (RFC 8058). Recipients who don't want incident emails any more can unsubscribe from their inbox — Cronheart marks the channel as unverified and the channel owner sees that in their dashboard.
Outgoing webhook contract
If you configured a generic webhook channel, alerts arrive as
POST requests with a JSON body and an HMAC-SHA256
signature over the raw body:
X-Cronheart-Signature: sha256=<hex>— verify withhash_hmac('sha256', $rawBody, $yourSecret)andhash_equals. Reject the request if it doesn't match.User-Agent: Cronheart-Webhook/1.0— useful as a receiver-side log filter.Content-Type: application/json
Body shape (stable schema, additive changes only):
{
"schema": 1,
"event": "alert",
"sent_at": "2026-05-06T08:00:00+00:00",
"alert": {
"id": 1234,
"kind": "late",
"urgent": true,
"color": "#f59e0b",
"created_at": "2026-05-06T07:59:30+00:00"
},
"monitor": {
"uuid": "f4a1...",
"name": "morning-batch",
"status": "late"
},
"subject": "[Late] morning-batch · Cronheart",
"preview": "\"morning-batch\" missed its expected ping window.",
"facts": [
{"label": "Status", "value": "late"},
{"label": "Schedule", "value": "cron \"*/5 * * * *\" (UTC)"}
],
"body": "<email-flavoured plain-text body>"
}
kind is one of late /
fail / recovered. urgent is
false for recovered, true for late and
fail. color is the same hex Cronheart paints in the
email badge / Slack stripe / Discord embed, so receivers that
forward to another surface stay visually consistent.
Status badges
Every monitor exposes a public read-only SVG at
/badge/<uuid>.svg. Drop it into your README to
show the world your nightly-rollup is healthy.

The badge URL is the only public surface that exposes the monitor UUID. We deliberately don't expose ping metadata in the badge — only the colour-coded status — so a stolen badge URL can't be replayed as a ping.
Security model
Monitor UUIDs are treated as per-monitor secrets and never logged at INFO+ on the request path. Audit logs (login, failed logins, impersonation, admin actions) live in a dedicated channel and rotate daily. Email and pings transit over TLS only.
Looking for our Privacy Policy, Cookie Policy, or Terms of Service? Those live on dedicated pages — they are part of the contract you have with Cronheart, and we keep them out of the fast-moving developer docs.