# Heartbeats

A scheduled job fails quietly: nothing is down, it just didn’t run. A heartbeat turns that round. Your job calls OpenPing when it finishes, and you hear when the call doesn’t come.

## One URL per job

Create a heartbeat monitor and it gets its own URL, shown on the monitor’s page. The token in the URL is the only credential, so no API key is needed. Treat the URL like a password.

| Call | Means |
| --- | --- |
| `https://hb.openping.ai/YOUR_TOKEN` | The job finished fine |
| `https://hb.openping.ai/YOUR_TOKEN/start` | The job started. Lets us tell you when it runs too long |
| `https://hb.openping.ai/YOUR_TOKEN/fail` | The job failed. The body may carry the exit code and the last lines of output |

`GET` and `POST` both work. Each answers `{ "ok": true }`, or 404 for a token we don’t know.

## From a shell script

backup.sh

```bash
HB=https://hb.openping.ai/YOUR_TOKEN

curl -fsS -m 10 "$HB/start" > /dev/null
if ./backup.sh > /tmp/backup.log 2>&1; then
  curl -fsS -m 10 "$HB" > /dev/null
else
  tail -n 20 /tmp/backup.log | curl -fsS -m 10 --data-binary @- "$HB/fail" > /dev/null
fi
```

## Or let the CLI wrap it

This calls start, runs your command, then reports the end or the failure with the exit code and the last lines of output:

```bash
openping run --heartbeat nightly-backup -- ./backup.sh
```

It exits with your command’s exit code, so nothing else about the job changes.

## Say when it should run

Give a cron expression with a time zone, or “every N minutes”, plus a grace period.

openping.yml

```yaml
monitors:
  - name: Nightly backup
    type: heartbeat
    schedule: "0 2 * * *"
    timezone: Asia/Kolkata
    grace: 15m
```

## When you hear about it

- **Late:** the expected time plus the grace period has passed with no call.
- **Failed:** the job called `/fail`.
- **Ran too long:** it called `/start` and has not finished within the limit you set.

A heartbeat has no regions, so one late or failed run is enough; there is nothing to confirm from elsewhere. The first 2 KB of output sent to `/fail` is kept, with personal data removed.
