# Monitors & schedules

A monitor is either a **Heartbeat** (your job reports to Tickingora) or an **HTTP Check** (Tickingora calls your endpoint). Both share the same schedule, calendar and alerting model.

```json
{
  "type": "heartbeat",
  "name": "Bank statement import",
  "description": "MT940 import and payment matching",
  "timezone": "Europe/Warsaw",
  "schedule": { "preset": "daily", "days": [1,2,3,4,5], "start": "06:00", "end": "07:00" },
  "calendar": { "skip_holidays": true, "excluded_dates": ["2026-12-31"] },
  "notifications": { "extra_email": "finance@example.com", "on_failure": true, "on_recovery": true }
}
```

| Field | Required | Notes |
| --- | --- | --- |
| `type` | yes (create) | `heartbeat` or `http`. Cannot be changed later. |
| `external_id` | no | Your stable technical id, unique in the account, up to 100 characters (`A–Z a–z 0–9 . _ : / -`), e.g. `production:nightly-backup`. |
| `name` | yes | Up to 120 characters; shown in the dashboard and alerts. |
| `description` | no | Up to 500 characters. |
| `timezone` | no | IANA name; defaults to the account time zone. The schedule is interpreted in it. |
| `schedule` | yes | One of three presets (below). |
| `calendar` | no | `skip_holidays` (Polish public holidays, default `false`), `excluded_dates` (`YYYY-MM-DD`). |
| `http` | for `http` | See [HTTP Checks](http-checks). |
| `notifications` | no | One `extra_email` besides the account e-mail (it receives alerts after confirming the address — see [Incidents & alerts](alerts#confirming-the-extra-address)); `on_failure` / `on_recovery` toggles. |
| `recovery` | no | HTTP only; see [HTTP Checks](http-checks). |

Unknown fields are rejected with `VALIDATION_ERROR`, so a typo such as `"shedule"` never silently falls back to a default.

## Finding monitors by `external_id`

Pick an `external_id` that names the job, not the moment — `production:nightly-backup`, `acme-shop/invoice-export`. Then:

- `GET /monitors?external_id=production:nightly-backup` finds it;
- creating another monitor with the same `external_id` returns `409 MONITOR_ALREADY_EXISTS` with `details.monitor_id` — so a set-up script or an agent can run again at any time without creating duplicates;
- `PATCH` can change it (or set `null`); deleting a monitor frees its `external_id`.

The id is not a secret and is shown in the dashboard. Monitors created in the dashboard have no `external_id` until you set one with `PATCH`.

## Schedules

Days are ISO weekdays: `1` = Monday … `7` = Sunday. Times are `HH:MM` in the monitor's time zone.

**Heartbeat windows are half-open `[start, end)`**: a signal at 06:59:59 counts for `06:00–07:00`; a signal at exactly 07:00 belongs to the next window (if there is one). HTTP Checks are points in time and have no `end`.

### `daily` — one window (or check) per selected day

```json
{ "preset": "daily", "days": [1,2,3,4,5], "start": "06:00", "end": "07:00" }
```

For an HTTP Check omit `end`: `{ "preset": "daily", "days": [1,2,3,4,5], "start": "07:00" }`.

A Heartbeat window may cross midnight, e.g. `"start": "23:30", "end": "00:30"`; it belongs to the day it starts on.

### `hourly` — every hour between two hours

```json
{ "preset": "hourly", "days": [1,2,3,4,5], "hour_from": 8, "hour_to": 18, "minute_start": 0, "minute_end": 15 }
```

Windows 08:00–08:15, 09:00–09:15 … 18:00–18:15. `hour_to` is the **last hour that has a window**. For Heartbeats, `minute_end` equal to `minute_start` means a full-hour window (08:00–09:00, 09:00–10:00, …). HTTP Checks omit `minute_end`.

### `custom` — explicit times

```json
{ "preset": "custom", "days": [1,2,3,4,5], "windows": [
  { "start": "06:00", "end": "07:00" },
  { "start": "12:00", "end": "12:30" }
] }
```

Up to 24 windows per day; HTTP Checks give only `start` for each entry.

### Rules enforced on every schedule

- Windows of one monitor **must not overlap**.
- Free plan: at most **one window or check per 60 minutes**.
- Each preset accepts only its own fields; others are reported as errors.

### Calendar

- `skip_holidays: true` skips Polish public holidays (New Year, Epiphany, Easter Sunday and Monday, 1 and 3 May, Pentecost, Corpus Christi, 15 August, 1 and 11 November, 24–26 December).
- `excluded_dates` adds your own days off.
- On excluded days nothing is expected and nothing alerts.

### Daylight saving time

Schedules follow wall-clock time in the monitor's time zone, with deterministic rules:

- A start time that does not exist (spring forward) moves forward by the gap — 02:30 becomes 03:30.
- A start time that happens twice (fall back) uses its first occurrence.
- Window length is real time: a 60-minute window is always 60 minutes.

`next_runs` in every monitor response shows the next five windows (or checks) in UTC, with holidays, exclusions and DST already applied — the quickest way to confirm a schedule.

## Updating with PATCH

`PATCH /monitors/{id}` accepts any subset of the fields above:

- top-level fields and the fields inside `calendar`, `http`, `notifications` and `recovery` replace their current values;
- `schedule` and arrays (such as `http.headers` or `calendar.excluded_dates`) are replaced **as a whole**;
- the change applies **from the next window** — the window in progress and all history stay as they were.

```bash
curl -sS -X PATCH https://dev.tickingora.com/api/v1/monitors/42 \
  -H "Authorization: Bearer $TICKINGORA_API_KEY" -H "Content-Type: application/json" \
  -d '{ "schedule": { "preset": "daily", "days": [1,2,3,4,5,6,7], "start": "01:30", "end": "03:00" } }'
```

## Status

| Monitor `status` | Meaning |
| --- | --- |
| `waiting` | Created or resumed; no window has finished yet. |
| `healthy` | The last finished window/check succeeded. |
| `alerting` | An incident is open. |
| `paused` | Nothing is expected; no alerts. |
| `unknown` | The last result could not be evaluated because of a Tickingora-side problem. |

Pausing (`POST /monitors/{id}/pause`) removes upcoming windows and closes an open incident without a recovery e-mail. Resuming counts towards the plan's active-monitor limit. Deleting stops monitoring immediately and revokes the ping URL.

## Plan limits (Free)

| Limit | Free |
| --- | --- |
| Active Heartbeat monitors | 20 |
| Active HTTP Checks | 3 |
| Most frequent schedule | every 60 minutes |
| Extra headers per HTTP Check | 5 |
| Recovery check interval | 15, 30 or 60 minutes |
| Detailed history | 30 days |

The same limits apply to the dashboard and the API. Exceeding one returns `403 PLAN_LIMIT_REACHED`; `GET /account` shows current usage.
