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. PHP and WordPress get first-class wrappers either way: 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. Drives alert delivery immediately — 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 transitions to late
or down — no fixed-window batching, so the
time-to-page is "the scanner sweep that detected it" (≤ 30 s) plus
the channel transport latency. Anti-flap dedupe makes sure a
recovering job doesn't double-page you.
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.