Pure HTTPS. Bearer token. JSON in, JSON out. No client library to keep in step with our release cadence, no version skew, no four-step OAuth dance to copy off Stack Overflow at 2 a.m.
Everything in the dashboard maps to one HTTP call you can make from curl,
an LLM tool call, a shell script, or a cron job. PAT scopes for least-privilege,
daily caps for agent blast-radius, Idempotency-Key for safe retries,
and per-PAT attribution in the audit log so a human can always answer "what did the
agent actually do?"
https://api.24observe.com
All endpoints are under /api/v1/. Self-hosters substitute their own host.
Interactive reference at /docs/openapi — every endpoint with schemas + try-it-now, rendered live from the same OpenAPI spec the API publishes. Always in sync with what's deployed.
openapi.json
— raw spec for generating a typed client in any language. Response shapes are declared
for the core agent-facing endpoints (PAT mint, monitor CRUD, monitor
secrets) so generators like openapi-typescript produce
real types, not any. Browsable Swagger UI at
api.24observe.com/docs.
Every request carries a bearer token in the Authorization header:
Authorization: Bearer obs_AbCdEf...
Tokens are personal access tokens (PATs): prefix obs_
followed by 43 url-safe base64 chars (256 bits of entropy). Stored as
SHA-256 hash; the plaintext is returned only once at creation.
# Minimal — wildcard scope, no caps (backward-compat default).
curl -X POST https://api.24observe.com/api/v1/me/tokens \
-H 'Authorization: Bearer obs_existing...' \
-H 'Content-Type: application/json' \
-d '{"name":"aeoniti-bot","expiresAt":"2027-01-01T00:00:00Z"}'
# Narrowly scoped + capped — recommended for agents.
curl -X POST https://api.24observe.com/api/v1/me/tokens \
-H 'Authorization: Bearer obs_existing...' \
-H 'Content-Type: application/json' \
-d '{
"name": "aeoniti-bot",
"scopes": ["monitors:write", "logs:write"],
"dailyMutationLimit": 500,
"dailyLogBytesLimit": 104857600
}'
# Response (201)
{
"id": 42,
"name": "aeoniti-bot",
"prefix": "obs_AbCdEfGh",
"token": "obs_AbCdEfGh...<43 chars total>",
"expiresAt": "2027-01-01T00:00:00Z",
"createdAt": "2026-05-13T18:00:00Z"
}
# List your tokens (no plaintext returned; includes scopes + caps)
curl https://api.24observe.com/api/v1/me/tokens -H 'Authorization: Bearer obs_...'
# Revoke (soft-delete; auth fails immediately for that token)
curl -X DELETE https://api.24observe.com/api/v1/me/tokens/42 \
-H 'Authorization: Bearer obs_...'
Scopes are an additional narrowing on top of the role check — the
request must pass both (role allows it AND scope allows it).
Default scopes: ["*"] = wildcard, inherits the role fully
(backward-compat). Resource wildcards work too: monitors:*
grants monitors:read + monitors:write.
The 24 valid narrow scopes (plus the wildcard):
* — wildcard (full role inheritance)monitors:read — list / read monitors (public projection)monitors:write — create / update / delete monitors, test alertssecrets:read — gates /monitors/:id/secrets (alert URLs, heartbeat token)incidents:read — list / read incidentsincidents:write — acknowledge / resolve / post updates / write postmortemsstatus-pages:read — list / read status pages + componentsstatus-pages:write — create / update / delete status pages, components, subscriberslogs:read — search + live-tail log eventslogs:write — ingest log events (HTTP + OTLP)metrics:read — query OTLP metric names + bucketed time seriesmetrics:write — ingest OTLP metricsaudit:read — read the org audit logtokens:write — mint additional PATs (for agent-bootstraps-agent flows)maintenance:read — list maintenance windowsmaintenance:write — schedule / cancel maintenance windowswebhooks:read — list event-webhook subscriptions + their delivery historywebhooks:write — create / update / delete event-webhook subscriptionssaved-searches:read — list saved log searchessaved-searches:write — create / update / delete saved searcheson-call:read — read on-call schedules + rotationson-call:write — manage on-call schedules + rotationsescalations:read — read escalation policiesescalations:write — manage escalation policiescontext:read — read the operational context graph (entities, neighborhoods, incident blast-radius)
A request that hits a route requiring a scope the PAT doesn't have
returns 403 with code: PAT_SCOPE_INSUFFICIENT.
Every 4xx response carries a stable code string your agent can
branch on. The message is for humans; the code is the API surface.
PAT_SCOPE_INSUFFICIENT — token doesn't have the required scope. Don't retry; mint a new one with the right scope.PAT_DAILY_LIMIT_EXCEEDED — per-token daily cap exhausted. Wait until X-PAT-Mut-Reset (unix seconds) or page a human. Response carries Retry-After.PLAN_LIMIT_LOGS_VOLUME — org's plan-level monthly log byte cap exhausted. Upgrade or stop ingesting until next billing period.CH_QUOTA_EXCEEDED — search or pattern query exceeded your plan's per-query CH memory or execution-time cap. Plan-tier guardrail; narrow the window, add filters, or upgrade.PLAN_LIMIT_MONITORS / PLAN_INTERVAL_TOO_LOW — plan-tier guardrails on monitor count / minimum interval.IDEMPOTENCY_KEY_REPLAY_CONFLICT — same key + different body. Your retry loop has a bug.IDEMPOTENCY_IN_PROGRESS — a request with this key is still running; back off and try again in a few seconds.WEBHOOK_URL_UNSAFE — the URL you tried to register resolves to a private / loopback / link-local / metadata address.WEBHOOK_URL_UNRESOLVABLE — the URL's hostname doesn't resolve (DNS NXDOMAIN, timeout, etc.). Distinct from SSRF — might come back later.MONITOR_TARGET_UNSAFE — same SSRF guard, applied to monitor target URLs at write time.REGISTRATION_BLOCKED / TURNSTILE_FAILED — account-creation guardrails.
Codes are stable across deploys: we will never repurpose an existing
code, only add new ones. An agent that branches on
PAT_DAILY_LIMIT_EXCEEDED today will still match it next year.
Optional fields on PAT creation; null / omitted = no cap.
dailyMutationLimit (int) — max POST / PATCH / DELETE / PUT calls per UTC day. Counter resets at midnight UTC. Hitting the cap returns 429 with code: PAT_DAILY_LIMIT_EXCEEDED. The counter only bumps on 2xx responses, so failed validation doesn't burn budget.dailyLogBytesLimit (int, bytes) — max log ingest volume per UTC day for this PAT. Same 429 + code. Slight over-shoot tolerated (bumped after a successful ingest; the over-shoot is bounded by one request's worth of bytes ≤ 1 MB).A PAT inherits the role its owner has in the org. If the owner is removed from the org, the PAT stops working immediately — no separate revoke needed.
Pass a client-generated UUID via the Idempotency-Key header
on any mutating request (POST / PATCH / DELETE / PUT). Server caches the
response keyed by (PAT, key) for 24h — retries return the
same response without re-executing. Standard Stripe-style semantics.
KEY=$(uuidgen)
curl -X POST https://api.24observe.com/api/v1/monitors \
-H 'Authorization: Bearer obs_...' \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{...}'
# On replay, response header: Idempotent-Replayed: true
Two concurrent requests with the same key get 409 with
code: IDEMPOTENCY_IN_PROGRESS — back off + retry. Keys
capped at 255 chars (IDEMPOTENCY_KEY_TOO_LONG on overflow).
5xx responses are NOT cached — retries genuinely re-run.
/heartbeats/:token): 60 / minute / IP./status-pages/public/:slug/subscribe): 5 / 10 min / IP.429 with Retry-After.Rate-limit counters are shared across API instances (not per-machine). Plus per-org plan caps (monitors, monthly checks, monthly log bytes) and per-PAT daily caps (see above).
All non-2xx responses follow this shape:
{
"error": "human-readable message",
"code": "MACHINE_READABLE_CODE", // optional but present on most 4xx
"fields": [ // present when code = VALIDATION_FAILED
{
"path": "intervalSec",
"code": "invalid_literal",
"message": "Expected one of 30, 60, 300, 900, 1800, 3600"
}
]
} Codes worth handling explicitly:
VALIDATION_FAILED (400) — request body or query failed schema validation. Inspect fields[] for the specific paths.PAT_SCOPE_INSUFFICIENT (403) — your PAT's scopes don't allow this route.PAT_DAILY_LIMIT_EXCEEDED (429) — daily mutation or log-bytes cap reached for this PAT. Resets UTC midnight.IDEMPOTENCY_IN_PROGRESS (409) — another request with the same key is in flight. Back off + retry.IDEMPOTENCY_KEY_TOO_LONG (400) — key > 255 chars.PLAN_LIMIT_MONITORS (429) — over your plan's monitor count cap.PLAN_INTERVAL_TOO_LOW (400) — interval below your plan's minimum.PLAN_LIMIT_LOGS_VOLUME (429) — org over monthly log byte cap.CH_QUOTA_EXCEEDED (429) — your search or pattern query exceeded the per-plan ClickHouse memory or execution-time cap. Free: 256 MiB / 3 s. Startup: 1 GiB / 10 s. Pro: 2 GiB / 30 s. Narrow the time range, add a service/level filter, or upgrade the plan.LOG_ENTROPY_REJECTED (202 with rejected[]) — single event exceeded the 5.7 bits/char entropy threshold (looks like base64, encrypted, or packed binary). Re-encode as structured JSON or ship to object storage instead.QUERY_LOG_READ_FAILED (500) — the /admin/query-stats endpoint couldn't read its telemetry table. Internal; transient.DUPLICATE (202 with rejected[]) — same event_id submitted within the 60s idempotency window. Safe to ignore; the original was kept. Response header Logs-Deduped counts how many in the batch were duped.ARCHIVE_NOT_CONFIGURED (503) — search window includes archived (>7d) data but the cold-tier R2 S3 credentials aren't set on the server. Operator action only.BAD_COMPRESSED_BODY (400) — request body had a Content-Encoding header (gzip / deflate / br) but the compressed payload was truncated, corrupt, or used the wrong format. Re-encode and retry. The API decompresses gzip/deflate/br request bodies natively for ingest endpoints — typical wire-size reduction is 8-20× on log batches.ALERT_URL_BLOCKED (400) — alert URL resolves to a private / loopback / metadata IP (SSRF defense).ALERT_URL_INVALID (400) — alert URL malformed or non-http(s).BAD_TIME_RANGE (400) — from > to on search.Full reference is in the OpenAPI spec; this is a sufficient map for most agent integrations. Each bullet is one route group.
GET /api/v1/monitors # public projection (no secrets). ?tag=foo&tag=bar filters by tag (AND)
POST /api/v1/monitors # scope: monitors:write
GET /api/v1/monitors/:id
PATCH /api/v1/monitors/:id # scope: monitors:write
DELETE /api/v1/monitors/:id # scope: monitors:write
GET /api/v1/monitors/:id/secrets # scope: secrets:read (owner/admin)
POST /api/v1/monitors/:id/test-alert # send a sample alert through every configured channel
POST /api/v1/monitors/:id/rotate-heartbeat-token
# Bulk operations (max 100 per request)
POST /api/v1/monitors/bulk # { "monitors": [MonitorCreate, ...] }
PATCH /api/v1/monitors/bulk # { "ids": [1,2,3], "patch": MonitorUpdate }
DELETE /api/v1/monitors/bulk # { "ids": [1,2,3] } (owner/admin only)
# Stats (time-series)
GET /api/v1/monitors/:id/checks # recent check results
GET /api/v1/monitors/:id/stats # uptime % / response-time stats
GET /api/v1/monitors/:id/timeline # bucketed timeline for the dashboard
# Multi-region (D8 MVP): set monitor.regions[] to opt-in
# Default ['local']; remote regions ['weur','enam','apac','oc','sam','afr']
# only HTTP/HTTPS today, more check types queued GET /api/v1/incidents GET /api/v1/incidents/:id POST /api/v1/incidents/:id/updates # post a public timeline update POST /api/v1/incidents/:id/acknowledge POST /api/v1/incidents/:id/resolve PUT /api/v1/incidents/:id/postmortem # rich-text markdown
# Authed (owner/admin) GET /api/v1/status-pages POST /api/v1/status-pages # scope: status-pages:write GET /api/v1/status-pages/:id PATCH /api/v1/status-pages/:id # scope: status-pages:write DELETE /api/v1/status-pages/:id # scope: status-pages:write PUT /api/v1/status-pages/:id/password # set / clear password (Argon2id) GET /api/v1/status-pages/:id/subscribers DELETE /api/v1/status-pages/:id/subscribers/:sid # Public (no auth) GET /api/v1/status-pages/public/:slug # JSON for the renderer GET /api/v1/status-pages/public/:slug/feed.xml # Atom feed GET /api/v1/status-pages/public/by-host/:host # custom-domain lookup POST /api/v1/status-pages/public/:slug/unlock # password unlock POST /api/v1/status-pages/public/:slug/subscribe # email subscribe GET /api/v1/status-pages/public/:slug/subscribe/confirm GET /api/v1/status-pages/public/unsubscribe
GET /api/v1/maintenance-windows POST /api/v1/maintenance-windows # scope: maintenance:write GET /api/v1/maintenance-windows/:id DELETE /api/v1/maintenance-windows/:id # scope: maintenance:write
GET /api/v1/me # current user + plan + quota GET /api/v1/me/members PATCH /api/v1/me/members/:userId # change role (owner only; can't demote sole owner) GET /api/v1/me/webhook-secret # per-org HMAC secret for outbound webhook signatures POST /api/v1/me/webhook-secret/rotate POST /api/v1/me/change-password # OAuth linking (Google + GitHub OIDC) GET /api/v1/me/oauth # which providers are linked GET /api/v1/me/oauth/google/start DELETE /api/v1/me/oauth/google GET /api/v1/me/oauth/github/start DELETE /api/v1/me/oauth/github # Invites GET /api/v1/me/invites POST /api/v1/me/invites # email invitee a join link DELETE /api/v1/me/invites/:id # PATs (see above for shape) GET /api/v1/me/tokens POST /api/v1/me/tokens # scope: tokens:write DELETE /api/v1/me/tokens/:id
GET /api/v1/audit-logs
?action=CREATE_MONITOR # exact action verb
&actor=42 # user id
&pat_id=17 # which PAT (Tier D fruit 1)
&from=2026-05-01T00:00:00Z
&to=2026-05-13T23:59:59Z
&limit=200
&format=csv # download as CSV
# Row shape
{
"id": 9123,
"createdAt": "2026-05-13T18:00:00Z",
"actorUserId": 42,
"actorPatId": 17, # null = session login; non-null = PAT
"actorEmail": "[email protected]",
"action": "UPDATE_MONITOR",
"resource": "monitor:88",
"meta": {
"changedFields": ["intervalSec","timeoutMs"],
"diff": {
"intervalSec": { "from": 60, "to": 300 },
"timeoutMs": { "from": 5000, "to": 10000 }
},
"ip": "203.0.113.7", # captured automatically (Tier E)
"ua": "AeonitiBot/1.0"
}
}
For sensitive-field PATCHes (alert URLs, telegram tokens, headers) the
diff entries are { "redacted": true } — values never
leak into audit. DELETE actions include a snapshot of key
identifying fields before the row was gone.
See /docs/logs for ingest, search, live tail, log-based alerts, OTLP/HTTP, and Vector config.
POST /api/v1/otlp/v1/logs # Standard OTLP/HTTP receiver. Set OTEL_EXPORTER_OTLP_ENDPOINT=https://api.24observe.com/api/v1/otlp # and any OTel SDK ships logs here. JSON encoding only in v1 (use the 24observe # collector image for http/protobuf). See /docs/logs#opentelemetry-otlphttp.
For non-OTel sources (Syslog, Fluent Bit), see the 24observe collector.
See /docs/heartbeats for receive URL, two-step token retrieval, and end-to-end examples.
GET /api/v1/badge/monitors/:id.svg # status badge (shields.io-style) POST /api/v1/heartbeats/:token # heartbeat receive HEAD /api/v1/heartbeats/:token # same, no body # Auth (no bearer required) POST /api/v1/auth/register POST /api/v1/auth/login POST /api/v1/auth/forgot-password POST /api/v1/auth/reset-password GET /api/v1/auth/google/start # OAuth GET /api/v1/auth/google/callback GET /api/v1/auth/github/start GET /api/v1/auth/github/callback
GET /health/live # cheap liveness (k8s pattern) GET /health/ready # all dependencies reachable GET /metrics # Prometheus GET /openapi.json # OpenAPI 3 spec GET /docs # Swagger UI
Programmatic equivalent of clicking "New monitor" in the dashboard.
60-second HTTPS check on https://example.com; alerts go to
Slack via webhook.
curl -X POST https://api.24observe.com/api/v1/monitors \
-H 'Authorization: Bearer obs_...' \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{
"name": "example.com homepage",
"url": "https://example.com",
"type": "https",
"intervalSec": 60,
"timeoutMs": 10000,
"alertSlackUrl": "https://hooks.slack.com/services/..."
}' import { randomUUID } from 'node:crypto';
const PAT = process.env.OBSERVE24_PAT!;
const res = await fetch('https://api.24observe.com/api/v1/monitors', {
method: 'POST',
headers: {
authorization: `Bearer ${PAT}`,
'idempotency-key': randomUUID(),
'content-type': 'application/json',
},
body: JSON.stringify({
name: 'example.com homepage',
url: 'https://example.com',
type: 'https',
intervalSec: 60,
timeoutMs: 10000,
alertEmail: '[email protected]',
}),
});
if (!res.ok) {
const err = await res.json();
// err.code: VALIDATION_FAILED / PLAN_LIMIT_MONITORS / PAT_SCOPE_INSUFFICIENT / ...
// err.fields: [{ path, code, message }] when VALIDATION_FAILED
throw new Error(`${res.status} ${err.code}: ${err.error}`);
}
const monitor = await res.json();
console.log('created monitor', monitor.id); import os, uuid, httpx
PAT = os.environ['OBSERVE24_PAT']
r = httpx.post(
'https://api.24observe.com/api/v1/monitors',
headers={
'authorization': f'Bearer {PAT}',
'idempotency-key': str(uuid.uuid4()),
'content-type': 'application/json',
},
json={
'name': 'example.com homepage',
'url': 'https://example.com',
'type': 'https',
'intervalSec': 60,
'timeoutMs': 10000,
'alertEmail': '[email protected]',
},
)
r.raise_for_status()
print('created monitor', r.json()['id']) Strict-validated; unknown fields are rejected. Full schema lives in the OpenAPI spec; the table below is the practical subset.
name (string, 1-255) — display name.url (string, 1-2048) — what to check.type — one of http, https, tcp, ping, port, ssl, keyword, heartbeat, dns, smtp.intervalSec — one of 30, 60, 300, 900, 1800, 3600. Plan tier sets the floor.timeoutMs — 1000-30000.headers (optional, ≤ 20 keys, encrypted at rest)expectedStatusCode, degradedResponseTimeMs, port, keyword, keywordMatchTypealertEmail, alertWebhookUrl, alertSlackUrl, alertDiscordUrl, alertMsteamsUrl, alertTelegramBotToken, alertTelegramChatId, alertPagerdutyRoutingKey, alertOpsgenieApiKey. URLs are SSRF-validated at write time and all secret-bearing fields are stored encrypted at rest.alertPagerdutyRoutingKey to your "Integration Routing Key" from a PagerDuty Events API v2 integration. We POST to events.pagerduty.com; the resolve event fires automatically when the monitor goes back up, so PD auto-closes the incident.alertOpsgenieApiKey to a GenieKey from an Opsgenie API integration. Down → P1 alert with deterministic alias; up → close-by-alias so the original alert resolves cleanly.tags to an array of lowercase slugs (e.g. ["prod","team-payments","critical"]) to group monitors. Max 20 per monitor. Filter by tag with GET /monitors?tag=prod&tag=team-payments (AND across multiple).POST /monitors/bulk takes up to 100 monitors in a single transaction (all-or-nothing). Bulk PATCH applies the same patch to many ids (useful for "add tag X to these 50 monitors"). Bulk DELETE removes up to 100 at once.alertThreshold — consecutive failures before an incident opens (default 1).regions (string array, default ["local"]) — multi-region opt-in. Local always runs; add any of weur / enam / apac / oc / sam / afr to dispatch additional checks from CF Workers. HTTP/HTTPS only in MVP.uptimeTargetBp (int, default 999 = 99.9%) — SLO target in basis points. Breach triggers an auto-incident on the scheduler tick.uptimeWindowDays (int, default 30) — SLO evaluation window. GET /api/v1/monitors and GET /api/v1/monitors/:id
return a public projection only — alert URLs, telegram tokens,
encrypted headers, and the plaintext heartbeat token are
not in the response. Boolean indicators (hasAlertWebhook,
hasAlertSlack, etc.) tell you which channels are configured.
To read the secrets:
GET /api/v1/monitors/:id/secrets Authorization: Bearer obs_<owner-or-admin PAT with secrets:read scope>
Every call is recorded in the audit log
(action: READ_MONITOR_SECRETS) with IP + user-agent +
actorPatId so the admin can prove which integration fetched what.