◉
24observe
DOCUMENTATION • API REFERENCE • 5 MIN READ

The spec is the SDK.

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.

Start free → Quickstart

TL;DR

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.

Docs · API

The spec is the SDK.

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?"

Base URL

https://api.24observe.com

All endpoints are under /api/v1/. Self-hosters substitute their own host.

Machine-readable spec

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.

Authentication

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.

Mint a PAT (with optional scopes + daily caps)

# 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_...'

PAT scopes

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 alerts
  • secrets:read — gates /monitors/:id/secrets (alert URLs, heartbeat token)
  • incidents:read — list / read incidents
  • incidents:write — acknowledge / resolve / post updates / write postmortems
  • status-pages:read — list / read status pages + components
  • status-pages:write — create / update / delete status pages, components, subscribers
  • logs:read — search + live-tail log events
  • logs:write — ingest log events (HTTP + OTLP)
  • metrics:read — query OTLP metric names + bucketed time series
  • metrics:write — ingest OTLP metrics
  • audit:read — read the org audit log
  • tokens:write — mint additional PATs (for agent-bootstraps-agent flows)
  • maintenance:read — list maintenance windows
  • maintenance:write — schedule / cancel maintenance windows
  • webhooks:read — list event-webhook subscriptions + their delivery history
  • webhooks:write — create / update / delete event-webhook subscriptions
  • saved-searches:read — list saved log searches
  • saved-searches:write — create / update / delete saved searches
  • on-call:read — read on-call schedules + rotations
  • on-call:write — manage on-call schedules + rotations
  • escalations:read — read escalation policies
  • escalations:write — manage escalation policies
  • context: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.

Stable error codes (the contract)

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 — account-creation guardrail (rate limit + duplicate-email).
  • BAD_JSON — the request body was not valid JSON. Returned in place of the raw parser message so no internal detail leaks.

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.

Per-PAT daily caps

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).

Roles (still apply)

  • owner / admin — every endpoint, including secret-bearing reads
  • member — create / edit monitors + status pages, view incidents
  • viewer — read-only

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.

Idempotency-Key (safe retries)

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.

Rate limits

  • Global: 100 requests / minute / IP across the API.
  • Logs ingest: 300 / minute / IP (separate bucket).
  • Logs search: 60 / minute / IP.
  • Logs tail (SSE): 30 / minute / IP.
  • Heartbeat receive (/heartbeats/:token): 60 / minute / IP.
  • Status subscribe (/status-pages/public/:slug/subscribe): 5 / 10 min / IP.
  • Hitting any limit returns 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).

Error envelope

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.
  • MONITOR_NAME_DUPLICATE (409) — a monitor with that exact name already exists in your org; pick a distinct name.
  • PLAN_LIMIT_LOGS_VOLUME (429) — org over monthly log byte cap.
  • CH_QUOTA_EXCEEDED (429) — your search or pattern query exceeded the per-plan query 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 object-storage 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 — log batches arrive at a small fraction of their raw size.
  • 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.

Endpoint index

Full reference is in the OpenAPI spec; this is a sufficient map for most agent integrations. Each bullet is one route group.

Monitors

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

Incidents

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

Status pages

# 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

Maintenance windows

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

Account / org

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

Audit log (scope: audit:read)

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": "alice@yourco.com",
  "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.

Logs (separate doc page)

See /docs/logs for ingest, search, live tail, log-based alerts, OTLP/HTTP, and Vector config.

OpenTelemetry (OTLP/HTTP)

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.

Heartbeats (separate doc page)

See /docs/heartbeats for receive URL, two-step token retrieval, and end-to-end examples.

Public / unauthenticated

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

Operational

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

Walkthrough: create a monitor

Programmatic equivalent of clicking "New monitor" in the dashboard. 60-second HTTPS check on https://example.com; alerts go to Slack via webhook.

curl

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/..."
  }'

TypeScript (Node 22 fetch)

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: 'alerts@yourco.com',
  }),
});

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);

Python (httpx)

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': 'alerts@yourco.com',
    },
)
r.raise_for_status()
print('created monitor', r.json()['id'])

Monitor request body — full shape

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, keywordMatchType
  • Alerts: alertEmail, 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.
  • PagerDuty: set 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.
  • Opsgenie: set 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: set 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).
  • Bulk migrations: 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.

Reading secret-bearing fields

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.

Other resources

Ready to build with 24Observe?

Explore live telemetry, zero-friction integration, and automated SOC investigation.

+ Get Free Trial / Demo