# HTTP check-ins (curl)

> Report runs from shell cron, CI or any language without an SDK with one curl command: start, finish, heartbeat.

Anything that can make an HTTPS request can report runs: a shell cron line, a CI step, a PHP script, a Go binary without an SDK. HTTP check-ins use the same URL format as Sentry's crons HTTP API, so existing scripts keep working with a Belvador DSN.

## The check-in URL

```text
https://ingest.belvador.dev/api/<project-id>/cron/<monitor-slug>/<public-key>/
```

`<project-id>` and `<public-key>` come from the project's DSN (`https://<public-key>@…/<project-id>`). The Monitors page shows the full URL for each monitor, already filled in. Treat it like the DSN: it can send data to your project but cannot read anything.

## Report a run with a start and a finish

Pick one id per run (32 hex characters) and send it with both requests, so Belvador can measure the duration and notice a run that started but never finished.

```bash
ID=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
curl -fsS -m 10 --retry 5 -o /dev/null "https://ingest.belvador.dev/api/<project id>/cron/nightly-invoices/<public key>/?status=in_progress&check_in_id=$ID" || true
```

When the job succeeds:

```bash
curl -fsS -m 10 --retry 5 -o /dev/null "https://ingest.belvador.dev/api/<project id>/cron/nightly-invoices/<public key>/?status=ok&check_in_id=$ID" || true
```

When it fails:

```bash
curl -fsS -m 10 --retry 5 -o /dev/null "https://ingest.belvador.dev/api/<project id>/cron/nightly-invoices/<public key>/?status=error&check_in_id=$ID" || true
```

A shell example that reports both cases:

```bash
if ./nightly-invoices.sh; then STATUS=ok; else STATUS=error; fi
curl -fsS "$URL?status=$STATUS&check_in_id=$ID"
```

## Or: a single heartbeat

When the work cannot report a start — a health loop, a worker that pings every few minutes — send one `ok` when it is done. Use an **Every N minutes/hours** monitor for this.

```bash
curl -fsS -m 10 --retry 5 -o /dev/null "https://ingest.belvador.dev/api/<project id>/cron/nightly-invoices/<public key>/?status=ok" || true
```

## Query parameters

| Parameter | Values |
|---|---|
| `status` | `in_progress`, `ok`, `error` (required) |
| `check_in_id` | 32 hex characters; the same id on the start and finish of one run |
| `environment` | e.g. `production` (default) or `staging` |
| `duration` | seconds, if you measure it yourself |
| `release` | your app's version |
| `outcome` + `reason` | with `status=ok`: `skipped` (nothing to do, healthy) or `unknown` (ended, effect unknown — **Needs review**); `reason` is a short code such as `nothing_due` |
| `run_id`, `attempt`, `will_retry` | group retries into one run: the same `run_id` on every attempt, `attempt` from 1, `will_retry=true` on a failed attempt your queue will repeat |

A finish without `check_in_id` closes the newest open start of that monitor.

A `POST` with a JSON body can also carry `monitor_config` — the same settings as `withMonitor` options, in Sentry's names (`schedule`, `timezone`, `checkin_margin`, `max_runtime`, `failure_issue_threshold`, `recovery_threshold`) — to create or update the monitor from the script.

## Answers

| Status | Meaning |
|---|---|
| `202` with `{"id": "…"}` | Accepted; `id` is the check-in id |
| `400` | Invalid slug, status or body |
| `401` | Unknown or inactive key, or the key belongs to another project |
| `403` | The workspace does not accept data right now |
| `413` | Body over 100 KiB |
| `429` | Too many check-ins: at most 60 per monitor and environment per minute, plus a monthly plan limit. Respect `Retry-After`. |

## Rules

- Never call this URL from a web page: browsers' check-ins are refused, and the key would be visible to every visitor.
- `curl -fsS` fails quietly on errors, so a Belvador outage never fails your job. Do not make your job depend on the check-in succeeding.
- If a check-in is refused for rate limits, the monitor shows **Not observable** rather than **Missed**.
