# Limits, pagination & idempotency

## Rate limits

Per API key: **60 requests per minute**, of which at most **20 writes** (`POST`, `PATCH`, `DELETE`). Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; above the limit you get `429 RATE_LIMITED` with `Retry-After` (seconds).

Heartbeat pings have their own, separate limits (see [Heartbeat](heartbeat)).

## Pagination

Lists (`/monitors`, `/monitors/{id}/runs`, `/monitors/{id}/incidents`) use cursors:

```json
{ "data": [ … ], "next_cursor": "eyJpZCI6NTAsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0", "has_more": true }
```

Pass `?cursor=<next_cursor>` for the next page and `?limit=` (1–100, default 50) for the page size. Cursors stay correct while new runs are being added. Stop when `has_more` is `false`.

```bash
cursor=""
while :; do
  page=$(curl -sS "https://dev.tickingora.com/api/v1/monitors?limit=100&cursor=$cursor" -H "Authorization: Bearer $TICKINGORA_API_KEY")
  echo "$page" | jq -r '.data[].name'
  [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
  cursor=$(echo "$page" | jq -r '.next_cursor')
done
```

Order: monitors by `id`; runs and incidents newest first. Runs can be filtered by `status`, `from` and `to` (ISO 8601).

## Idempotency

`POST /monitors` accepts an `Idempotency-Key` header (up to 255 characters). It protects **one request** against retries — a network timeout, a crashed worker — for **24 hours**:

- the first request is processed normally and its response stored for **24 hours**;
- a repeat with the same key and the same body returns the stored response with `Idempotent-Replayed: true` — no second monitor;
- the same key with a different body returns `422 IDEMPOTENCY_KEY_REUSED`;
- a repeat while the first request is still running returns `409 IDEMPOTENCY_IN_PROGRESS`.

Keys are scoped to your account. `PATCH`, `DELETE`, `pause` and `resume` are naturally idempotent.

The replayed response of a Heartbeat `POST /monitors` contains the one-time `ping_url`, so a retry after a lost response still gets it. To keep that safe, stored responses are **encrypted** and **deleted as soon as the 24 hours pass** — from then on the token exists only as a hash and cannot be recovered (use `rotate-token`).

After 24 hours the same key is processed as a new request. To make **repeated set-ups** (a script or an agent that runs again next week) safe for good, give the monitor an `external_id`: a second monitor with the same `external_id` is never created — the API answers `409 MONITOR_ALREADY_EXISTS` with `details.monitor_id`. Use both: `external_id` for identity, `Idempotency-Key` for retries.

## Plan limits and retention

Plan limits (active monitors, frequency, headers, recovery interval) are the same as in the dashboard — see [Monitors & schedules](monitors#plan-limits-free). Run and incident history is available for 30 days on the Free plan; older entries are deleted and do not appear in the API.
