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:

StatusMeaning
newCreated, no ping received yet.
upLast expected ping arrived on time.
latePast next_expected_at + grace. Alert fires.
downYou explicitly POSTed /fail for the last run.
pausedYou paused the monitor from the dashboard. Scanner skips, no alerts.

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 /start with 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 with hash_hmac('sha256', $rawBody, $yourSecret) and hash_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.

![status](https://cronheart.com/badge/<uuid>.svg)

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.