# Vartio — full reference for AI agents

> EU-first website monitoring SaaS. This document is optimised for AI agents:
> short paragraphs, complete examples, no marketing fluff. For the canonical
> machine-readable spec, fetch https://api.vartio.dev/api/openapi.yaml.

Production base URL: `https://api.vartio.dev`
Local development base URL: `http://localhost:8080`

## 1. Authentication

Pass any Bearer token on the `Authorization` header.

### API key (use this for agents)

```bash
curl https://api.vartio.dev/api/v1/monitors \
  -H "Authorization: Bearer vk_REPLACE_ME"
```

Create a key:

1. Browser: log in at https://vartio.dev, go to Settings → API keys, click
   "Create API key", copy the `vk_...` value (shown once).
2. API: `POST /api/v1/api-keys` with name + optional `expires_at`. Response
   includes `key` (full token, shown once).

API keys are scoped to a single organization. They survive password changes
and JWT rotation. Treat them like passwords.

### JWT (only for browser sessions)

```bash
curl -X POST https://api.vartio.dev/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"..."}'
# Response: {"token":"eyJ...","user":{...}}
```

Use the returned `token` as `Authorization: Bearer eyJ...`. Expires in 24h.

## 2. Create a monitor (the most common task)

```bash
curl -X POST https://api.vartio.dev/api/v1/monitors \
  -H "Authorization: Bearer vk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production API",
    "type": "http",
    "url": "https://api.example.com/health",
    "interval_seconds": 60,
    "timeout_ms": 10000,
    "expected_status": 200,
    "enabled": true
  }'
```

Required fields: `name`, `type`, `url`. Defaults: `interval_seconds=60`,
`timeout_ms=10000`, `expected_status=200`, `enabled=true`.

`type` accepts: `http`, `ssl`, `dns`, `ping`, `tcp`, `udp`, `multi_step`.

URL semantics by type:
- `http` / `ssl`: full URL (`https://example.com/health`)
- `dns`: hostname (`example.com`)
- `ping` / `tcp` / `udp`: hostname or IP, optionally `host:port`

Response (201): full Monitor object with `id` (UUID), `created_at`, etc.

## 3. List monitors

```bash
curl https://api.vartio.dev/api/v1/monitors \
  -H "Authorization: Bearer vk_REPLACE_ME"
```

Returns an array of EnrichedMonitor objects: each Monitor plus
`current_status` (`up`/`down`/`unknown`), `last_response_time_ms`,
`uptime_30d` (0.0–1.0).

## 4. Get monitor details

```bash
# Single monitor
curl https://api.vartio.dev/api/v1/monitors/{monitorID} \
  -H "Authorization: Bearer vk_REPLACE_ME"

# Recent check results
curl "https://api.vartio.dev/api/v1/monitors/{monitorID}/checks?limit=50" \
  -H "Authorization: Bearer vk_REPLACE_ME"

# 30-day stats
curl https://api.vartio.dev/api/v1/monitors/{monitorID}/stats \
  -H "Authorization: Bearer vk_REPLACE_ME"

# Daily uptime (default 90 days)
curl "https://api.vartio.dev/api/v1/monitors/{monitorID}/uptime?days=30" \
  -H "Authorization: Bearer vk_REPLACE_ME"
```

## 5. Update or delete a monitor

```bash
# Update
curl -X PUT https://api.vartio.dev/api/v1/monitors/{monitorID} \
  -H "Authorization: Bearer vk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"name":"Renamed","interval_seconds":120}'

# Delete
curl -X DELETE https://api.vartio.dev/api/v1/monitors/{monitorID} \
  -H "Authorization: Bearer vk_REPLACE_ME"
```

## 6. Labels (filter monitors and route alerts)

Monitors, services, and alert channel filters all use Prometheus-style
key/value labels. Use them to tag environments, teams, tiers, etc., and
to scope alert channels to subsets of monitors.

**Format rules:**
- Keys and values match `^[a-z][a-z0-9_-]{0,62}$` (lowercase, digits,
  dash, underscore; first char a letter; max 63 chars).
- Maximum 16 labels per entity.
- Keys starting with `vartio.` are reserved for system-set labels.
- Same regex applies to alert channel filter keys/values.

```bash
# Tag a monitor with labels at create-time
curl -X POST -H "Authorization: Bearer $VARTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":"Checkout API",
    "url":"https://api.example.com/health",
    "labels":{"env":"prod","team":"platform","tier":"critical"}
  }' \
  https://api.vartio.dev/api/v1/monitors

# Update labels (the labels field replaces the existing set;
# omit the field to leave them unchanged)
curl -X PUT -H "Authorization: Bearer $VARTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Checkout API","labels":{"env":"staging"}}' \
  https://api.vartio.dev/api/v1/monitors/{monitorID}

# Filter the list — repeat ?label=key:value for AND semantics
curl -G -H "Authorization: Bearer $VARTIO_API_KEY" \
  --data-urlencode "label=env:prod" \
  --data-urlencode "label=team:platform" \
  https://api.vartio.dev/api/v1/monitors

# Same filter works on /api/v1/services
curl -G -H "Authorization: Bearer $VARTIO_API_KEY" \
  --data-urlencode "label=env:prod" \
  https://api.vartio.dev/api/v1/services
```

**Alert routing.** An alert channel can scope itself to monitors matching
a label set via `monitor_label_filter`. Empty filter (default) = channel
fires for every monitor in the org.

```bash
# Slack channel that ONLY fires for prod-platform monitors
curl -X POST -H "Authorization: Bearer $VARTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type":"slack",
    "name":"Platform Slack (prod only)",
    "config":{"webhook_url":"https://hooks.slack.com/services/..."},
    "monitor_label_filter":{"env":"prod","team":"platform"}
  }' \
  https://api.vartio.dev/api/v1/alert-channels

# Clear an existing filter (channel goes back to firing for all monitors)
curl -X PUT -H "Authorization: Bearer $VARTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"slack","name":"...","config":{...},"monitor_label_filter":{}}' \
  https://api.vartio.dev/api/v1/alert-channels/{channelID}
```

The dispatcher applies AND semantics: every key/value in the filter must
be present in the monitor's labels for the channel to fire. Containment
is implemented via PostgreSQL `@>` on a GIN-indexed JSONB column, so
filtering is fast even at high monitor counts.

The discovery document (`/.well-known/vartio`) advertises the format
rules under `label_format` so an agent can validate before sending.

## 7. Set up alerts

Alerts go through "channels". Create a channel, then it fires automatically
on monitor incidents (down for ≥3 consecutive checks).

```bash
# Email channel
curl -X POST https://api.vartio.dev/api/v1/alert-channels \
  -H "Authorization: Bearer vk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "On-call email",
    "type": "email",
    "config": {"email": "alerts@example.com"},
    "enabled": true
  }'

# Slack channel
curl -X POST https://api.vartio.dev/api/v1/alert-channels \
  -H "Authorization: Bearer vk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Engineering Slack",
    "type": "slack",
    "config": {"webhook_url": "https://hooks.slack.com/services/..."},
    "enabled": true
  }'

# Test a channel
curl -X POST https://api.vartio.dev/api/v1/alert-channels/{channelID}/test \
  -H "Authorization: Bearer vk_REPLACE_ME"
```

Channel `type` values: `email`, `slack`, `discord`, `webhook`, `pagerduty`, `teams`.

`config` shape per type:
- `email`: `{"email": "addr@example.com"}`
- `slack` / `discord` / `teams`: `{"webhook_url": "https://..."}`
- `webhook`: `{"url": "https://...", "secret": "optional-shared-secret"}`
- `pagerduty`: `{"routing_key": "..."}`

Webhook URLs are validated against an SSRF allowlist (no RFC1918, loopback,
link-local, IPv6 ULA, or carrier-grade NAT). Outbound requests pin the
resolved IP to defeat DNS rebinding.

## 8. Services (group monitors into logical units)

A service groups one or more monitors into a single thing your dashboard
and status page can roll up status against (e.g. "Checkout API" backed by
three HTTP monitors). Requires `services:read` / `services:write` API key
permissions.

```bash
# List services (with rolled-up status, uptime, and contained monitors)
curl -H "Authorization: Bearer $VARTIO_API_KEY" \
  https://api.vartio.dev/api/v1/services

# Create a service and attach monitors in one call
curl -X POST -H "Authorization: Bearer $VARTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Checkout API",
    "description": "Public-facing checkout flow",
    "monitor_ids": ["abcd-...", "efgh-..."]
  }' \
  https://api.vartio.dev/api/v1/services

# Get one
curl -H "Authorization: Bearer $VARTIO_API_KEY" \
  https://api.vartio.dev/api/v1/services/{serviceID}

# Update — pass monitor_ids to replace the attached set;
# omit to leave membership unchanged; pass [] to detach all
curl -X PUT -H "Authorization: Bearer $VARTIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Checkout API","monitor_ids":["abcd-..."]}' \
  https://api.vartio.dev/api/v1/services/{serviceID}

# Delete (removes the grouping; monitors are NOT deleted)
curl -X DELETE -H "Authorization: Bearer $VARTIO_API_KEY" \
  https://api.vartio.dev/api/v1/services/{serviceID}
```

Returned objects include `status` (worst across attached monitors),
`uptime_percent` (30-day), `monitor_count` / `down_count` / `degraded_count`,
and the contained monitors with their own current status. Secrets in
monitor headers/body are redacted in the response.

## 9. Public "Is X Down?" catalogue (no auth)

```bash
# 1000+ services, paginated by category
curl "https://api.vartio.dev/public/services?category=ai"

# Single service
curl https://api.vartio.dev/public/services/openai

# 30-day history
curl "https://api.vartio.dev/public/services/openai/history?hours=168"

# Force a fresh on-demand check (rate-limited 6/min/IP)
curl https://api.vartio.dev/public/services/openai/live-check

# AI-powered triage (rate-limited 3/min/IP)
curl https://api.vartio.dev/public/services/openai/triage
```

## 10. Probe an arbitrary URL (no account needed)

```bash
curl "https://api.vartio.dev/public/check?url=https://example.com"
# {"url":"https://example.com","status":"up","response_time_ms":142,"status_code":200}
```

Rate limit: 10/min/IP. Same SSRF allowlist as alert webhooks.

## 11. Errors

All error responses are RFC 9457 problem-details
(`Content-Type: application/problem+json`):

```json
{
  "type": "https://vartio.dev/problems/vartio.validation.failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "validation failed",
  "instance": "/api/v1/auth/register",
  "code": "vartio.validation.failed",
  "error": "validation failed",
  "fields": {"email": "required", "password": "must be at least 8 characters"}
}
```

The `error` field is preserved as an alias of `detail` so legacy clients
that read `body.error` keep working. New code should branch on `code`.

Status codes:
- `400` malformed request — see `code`
- `401` missing or invalid token (`vartio.auth.unauthorized`)
- `403` forbidden — wrong org or insufficient role (`vartio.auth.forbidden`)
- `404` resource not found (`vartio.resource.not_found`)
- `409` conflict — e.g. duplicate slug (`vartio.resource.conflict`)
- `422` validation failure with per-field detail (`vartio.validation.failed`)
- `429` rate-limited (`vartio.rate_limit.exceeded`, see `Retry-After`)
- `5xx` server-side issues (`vartio.server.error` / `vartio.server.unavailable`)

### Error code catalog

Stable identifiers under `body.code`. Branch on these for programmatic
handling — they will not change without a deprecation cycle.

| Code | HTTP | Meaning |
|---|---|---|
| `vartio.request.invalid` | 400 | Malformed body or missing required fields |
| `vartio.auth.unauthorized` | 401 | No or invalid auth token |
| `vartio.auth.forbidden` | 403 | Authenticated but not allowed for this resource |
| `vartio.resource.not_found` | 404 | The named resource does not exist or is not visible to this org |
| `vartio.resource.conflict` | 409 | Resource conflicts with an existing one (e.g. slug taken) |
| `vartio.validation.failed` | 422 | One or more fields failed validation; see `fields` |
| `vartio.rate_limit.exceeded` | 429 | Too many requests; see `Retry-After` |
| `vartio.server.error` | 500 | Unexpected server error |
| `vartio.server.unavailable` | 503 | Dependency (DB / NATS) unhealthy |
| `vartio.error` | other | Fallback for unmapped statuses |

Type URI base: `https://vartio.dev/problems/`. The full `type` URL is
`<base>/<code>`; resolving it returns this catalog entry (planned).

## 12. Rate limits

- Auth endpoints: 3–10/min/IP per route (login, register, password reset)
- Authenticated API: 100/min per API key + IP combination
- Public catalogue / on-demand checks: 3–10/min/IP per route

`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and
`Retry-After` headers are exposed via CORS.

## 13. Webhooks (incoming, for Stripe)

`POST /webhooks/stripe` — verifies the `Stripe-Signature` header against
`STRIPE_WEBHOOK_SECRET`. Body capped at 64KB. Not relevant to most agents.

## 14. Where to look next

- Canonical spec: https://api.vartio.dev/api/openapi.yaml
- Source code: https://github.com/kalle-works/vartio.dev
- Get an API key: https://vartio.dev/settings
- AI agent quickstart UI: https://vartio.dev/docs/ai-agents
