24observe
checking… Sign in Start free
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):

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.

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.

Roles (still apply)

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

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:

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": "[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.

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: '[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);

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': '[email protected]',
    },
)
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.

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