# Farsight > Farsight is an uptime monitor: it checks websites, APIs, ports, hosts, DNS records, TLS certificates and heartbeats on a schedule, opens incidents, alerts people (email, Discord, webhooks) and publishes a status page. Everything the dashboard does is available over a JSON API, built for scripts and AI agents. ## Start here - Base URL: `https:///api/v1`. JSON in and out. - Auth: `Authorization: Bearer fs_<48 hex>`. A human makes the key in Settings, Security, API keys (scopes `read` < `write` < `admin`). Keys never manage sign-in factors and cannot make more keys. - Errors: `{"error": {"code", "message", "field"?, "fields"?: [{field, message}]}}`. On `422 invalid`, fix the named fields and retry. - Retries: send `Idempotency-Key: ` on POSTs; a repeat within 24 h returns the first answer. - Prefer `PUT /monitors/by-slug/{slug}` (create or replace) so running the same plan twice is harmless. - Rate limit: 600 requests a minute per key. ## Monitors A monitor is `{"name", "kind", "": {settings}, ...common}`. Kinds: - `http`: `{"url", "method"?: "GET", "headers"?: [{"name","value","secret"?}], "body"?, "auth"?: {"type":"basic","username","password"} | {"type":"bearer","token"}, "expect_status"?: "200-399", "body_contains"?, "body_not_contains"?, "json"?: [{"path":"$.status","op":"==","value":"ok"}], "expect_header"?: {"name","contains"?}, "max_ms"?, "follow_redirects"?: true, "tls_verify"?: true, "tls_warn_days"?: 14, "resolve_to"?, "ip_version"?: "auto"}` - JSON ops: `==` `!=` `<` `<=` `>` `>=` `contains` `exists` `not_exists`. Paths: `$.a.b[0].c`, `$['odd key']`. - A healthy endpoint that answers 401 (an API gateway without a key) can be watched with `"expect_status": "401"`. - `tcp`: `{"host", "port", "send"?, "expect_banner"?}` (e.g. `"expect_banner": "SSH-2.0"`). - `ping`: `{"host", "packets"?: 3, "max_loss_pct"?}`. - `dns`: `{"name", "record"?: "A"|"AAAA"|"CNAME"|"MX"|"TXT"|"NS", "resolver"?: "1.1.1.1", "expect"?}`. - `tls`: `{"host", "port"?: 443, "sni"?, "tls_warn_days"?: 14}`. - `heartbeat`: `{"grace_s"?: 60}`. The job calls the secret URL from `POST /monitors/{id}/heartbeat-token` every `interval_s`; `/hb/{token}/fail` reports a failure. Common fields (defaults): `slug` (from name), `group`, `tags`, `description`, `runbook_url`, `interval_s` (60; 10-86400), `timeout_ms` (10000), `confirm_after` (2 failed checks before Down), `retry_interval_s` (10), `recover_after` (1), `slow_ms`, `slow_after` (3), `alert_on_slow` (false), `channels` (null = all), `remind_every_min` (0), `paused`, `muted_until`, `public`, `public_name`, `public_group`, `sort`. Secrets come back as `"********"`; sending the mask back keeps the stored value only if the URL's scheme, host and port are unchanged. ## Common tasks ``` # Validate and try a monitor without saving it POST /api/v1/monitors/test {"name":"API","kind":"http","http":{"url":"https://api.example.com/health","json":[{"path":"$.ok","op":"==","value":true}]}} # Create or replace (idempotent) PUT /api/v1/monitors/by-slug/api {"name":"API","kind":"http","http":{"url":"https://api.example.com/health"},"interval_s":30,"group":"Production"} # Many at once (preview first) POST /api/v1/monitors/bulk {"dry_run":true,"monitors":[ ... ]} # What is broken right now GET /api/v1/monitors?status=down GET /api/v1/checks?limit=100&outcome=down (every monitor's checks, newest first, each with monitor_id) GET /api/v1/events?kind=down,up (what changed: state changes, path notes, config, sign-ins) GET /api/v1/incidents?state=open GET /api/v1/incidents/{id} (adds checks and a timeline: [{"at","kind","text"}] in plain sentences) # Quiet a monitor during work POST /api/v1/monitors/slug:api/mute {"minutes":60} POST /api/v1/monitors/slug:api/pause # History GET /api/v1/monitors/{id}/checks?limit=50 GET /api/v1/monitors/{id}/series?range=24h (1h, 24h, 7d, 30d, 90d) GET /api/v1/monitors/{id}/uptime?days=90 # The public status page: what it shows and how it looks PUT /api/v1/status-page {"monitors":[{"id":3,"public":true,"public_name":"Pesa","public_group":"Apps"}]} PUT /api/v1/status-page {"layout":"horizon","theme":"auto","accent":"#564be6","default_range":"90d","website":{"label":"example.com","url":"https://example.com"}} POST /api/v1/status-page/preview {"settings":{"layout":"classic"},"range":"7d"} (the page's HTML from a draft; nothing saved) PUT /api/v1/status-page {"app_name":"09 status"} (the name under the icon on a phone's home screen; empty = the title) PUT /api/v1/status-page/logo (raw image body) then optionally PUT /api/v1/status-page/icon (a square PNG, 180 to 1024 px, made from the logo; else phones show Farsight's icon) GET /api/public/status?range=7d (public JSON; pages at /status, /status/, /status/history) ``` `{id}` is a number or `slug:`. ## State Each monitor answer has `state` (`status`: up, down, slow, paused, unknown; `since`, `failing`, `flapping`, `muted`, `latency_ms`, `error`: {code, message}, `incident_id`, `cert_expires_at`) and `stats` (`uptime_24h`, `uptime_7d`, `uptime_30d`, `p50_ms_24h`, `p95_ms_24h`, `spark`). Error codes: dns_failed, refused, timeout, tls_invalid, status_mismatch, body_mismatch, json_mismatch, header_mismatch, too_slow, ping_loss, dns_mismatch, banner_mismatch, heartbeat_missing, heartbeat_fail, network, blocked, internal. Farsight confirms failures before calling something down, re-checks after `retry_interval_s`, groups alerts that arrive within 20 seconds into one message, holds alerts for a monitor that flaps, does not count failures while its own internet connection is down, and asks a second checker on Cloudflare before counting a failure where the target never answered (a network path problem is shown as `state.note`, not counted). ## More - Full contract: `/docs/api` (every route, body and answer). - OpenAPI: `/api/openapi.json`. - Live updates: `GET /api/v1/stream` (Server-Sent Events). - Private networks (loopback, RFC 1918, CGNAT/Tailscale) are blocked as targets unless the owner allows them in Settings.