REST API v1

A stable, versioned JSON API over the same operations the IsItStillUp dashboard and the MCP servers use: read and edit your pages and components, open, correct and close incidents, manage subscribers and incident templates, push system metrics, and pull uptime history. Every route lives under /api/v1 and speaks JSON in both directions, and the whole surface is described machine-readably at /api/v1/openapi.json.

Authentication

Every request carries an IsItStillUp API key as a bearer token: Authorization: Bearer bcn_.... A key belongs to exactly one organization — every response is scoped to that organization's own pages, components and incidents, and an id from any other organization is indistinguishable from an id that doesn't exist (both answer 404).

Create a key from your dashboard under Settings → API keys, or (for local development) with scripts/create-api-key.ts. The full secret is shown exactly once, at creation — IsItStillUp stores only its hash, so if you lose it, revoke it and mint a new one.

A key can be given an optional expiry date when you create it. After it passes, every request answers 401 unauthorized with the message API key expired — distinct from the message a revoked or mistyped key gets, so you can tell a rotation you forgot from a credential you fat-fingered. A key with no expiry works until it is revoked.

Each key has one or more scopes: read or write. write always implies read, so a read-only key can call every read endpoint below and gets a 403 forbidden on every write one. A missing or unrecognized key answers 401 unauthorized with a WWW-Authenticate header.

curl -sS "$BEACON_URL/api/v1/pages" \
  -H "Authorization: Bearer $BEACON_API_KEY"

Rate limits

Each API key gets a fixed one-minute window: up to its plan's request budget per minute, with the counter resetting when the window rolls (X-RateLimit-Reset counts the seconds). The rate comes from the key's organization's plan:

PlanPriceRequests / minute
freeFree60
starter$5/mo600
pro$15/mo600
business$29/mo1,200
enterprise$99/mo3,000

Every response — success or error — carries the current state of the bucket:

  • X-RateLimit-Limit — the bucket's capacity (requests).
  • X-RateLimit-Remaining — requests left in the bucket right now.
  • X-RateLimit-Reset — seconds until the bucket is back to full.

Exceeding it answers 429 rate_limited. Back off until X-RateLimit-Reset elapses rather than retrying immediately.

Errors

Every error response has the same shape, whatever the status: {"error":{"code","message"}}. message is meant to be read by a person; branch your code on code, not on the text of message.

{
  "error": {
    "code": "validation_error",
    "message": "Invalid request body — title: Required"
  }
}
StatuscodeMeaning
401unauthorizedMissing, malformed, or revoked API key.
403forbiddenValid key, but it lacks the "write" scope this endpoint needs.
404not_foundThe id doesn't exist, or doesn't belong to this key's organization.
422validation_errorMalformed JSON, a failed schema check, or a rule @/lib/core rejected (e.g. a bad status transition).
429rate_limitedThis API key's per-minute budget is exhausted. Check X-RateLimit-Reset.

Idempotency

POST /api/v1/pages/{pageId}/incidents accepts an Idempotency-Key header. Send the same key on a retry (a timeout, a dropped connection, anything that leaves you unsure whether the first attempt landed) and IsItStillUp returns the original incident instead of creating a second one — the response carries an Idempotent-Replay: true header and a 200 instead of 201 so you can tell the two apart.

  • Keys are scoped to your organization and remembered for 24 hours.
  • Use a fresh key (a UUID is a good choice) per logical incident-open attempt, not per request retry loop.
  • A request that fails validation is never cached — retrying with the same key after fixing the body creates the incident normally.

Endpoints

Scopes: read works with any valid key; write needs a key created with write access.

MethodPathScopeBody / query
GET/api/v1/pagesread—
GET/api/v1/pages/{pageId}read—
PATCH/api/v1/pages/{pageId}writename, description, timeZone, hiddenFromSearch, subscriptionOptions, branding — at least one
GET/api/v1/pages/{pageId}/componentsread—
POST/api/v1/pages/{pageId}/componentswritename (required), description, parentId, position
PATCH/api/v1/components/{id}writename, description, parentId, position, status, showUptime — at least one
DELETE/api/v1/components/{id}write—
GET/api/v1/components/{id}/uptimereaddays (1-365, default 90), or start & end (YYYY-MM-DD, capped at 365 days)
GET/api/v1/pages/{pageId}/incidentsreadq, status, kind (each comma-separated), include_drafts (default false), limit (1-500, default 50)
GET/api/v1/pages/{pageId}/incidents/unresolvedreadlimit (1-500, default 50)
GET/api/v1/pages/{pageId}/incidents/scheduledreadlimit (1-500, default 50)
GET/api/v1/pages/{pageId}/incidents/upcomingreadlimit (1-500, default 50)
GET/api/v1/pages/{pageId}/incidents/active_maintenancereadlimit (1-500, default 50)
POST/api/v1/pages/{pageId}/incidentswritetitle, body (required); kind, status, severity, componentIds, componentImpact, scheduledStart, scheduledEnd, metadata, publish
GET/api/v1/incidents/{id}read—
PATCH/api/v1/incidents/{id}writetitle, severity (null clears it), metadata — at least one
DELETE/api/v1/incidents/{id}write—
POST/api/v1/incidents/{id}/updateswritestatus, body (required); componentStatuses, publish
POST/api/v1/incidents/{id}/resolvewritebody (required)
PATCH/api/v1/incidents/{id}/updates/{updateId}writebody, displayAt (null clears the backdating override) — at least one
GET/api/v1/pages/{pageId}/subscribersreadq, channel (email | slack | teams | discord | sms), page (1-based)
POST/api/v1/pages/{pageId}/subscriberswritechannel, target (both required)
DELETE/api/v1/subscribers/{id}write—
GET/api/v1/pages/{pageId}/metricsread—
POST/api/v1/pages/{pageId}/metricswritename (required); suffix, decimals (0-6), published, position
PATCH/api/v1/metrics/{id}writename, suffix, decimals, published, position — at least one
DELETE/api/v1/metrics/{id}write—
POST/api/v1/metrics/{id}/datawrite{ value, ts? } for one sample, or { points: [{ ts?, value }, …] } for up to 3000
POST/api/v1/pages/{pageId}/metrics/datawrite{ "<metricId>": [{ ts?, value }, …] } — a single object may stand in for a one-element array
GET/api/v1/pages/{pageId}/templatesread—
POST/api/v1/pages/{pageId}/templateswritename, title, body (required); kind, severity, componentIds, componentImpact
GET/api/v1/templates/{id}read—
PATCH/api/v1/templates/{id}writename, kind, title, body, severity, componentIds, componentImpact — at least one
DELETE/api/v1/templates/{id}write—

$BEACON_URL is your IsItStillUp dashboard's own origin (the host your status pages are managed from — e.g. https://app.yourcompany.com). Ids in curly braces are path parameters; examples below use shell variables ($PAGE_ID, $COMPONENT_ID, $INCIDENT_ID, $UPDATE_ID, $SUBSCRIBER_ID, $TEMPLATE_ID, $METRIC_ID) for them.

Every endpoint on this page — the Metrics group included — is also described machine-readably at /api/v1/openapi.json (OpenAPI 3.1, no key required) — point a client generator or an HTTP client at it directly.

Pages

GET/api/v1/pagesread

List every status page this API key's organization owns.

curl -sS "$BEACON_URL/api/v1/pages" \
  -H "Authorization: Bearer $BEACON_API_KEY"
GET/api/v1/pages/{pageId}read

Fetch one status page and its settings.

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY"
PATCH/api/v1/pages/{pageId}write

Update a page's settings. branding and subscriptionOptions are merged key-by-key, so you can set one without restating the rest. Slug, custom domain and visibility are deliberately not writable here.

Body: name, description, timeZone, hiddenFromSearch, subscriptionOptions, branding — at least one

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -X PATCH -d '{"timeZone":"Europe/Berlin","branding":{"accent":"#0f766e"}}'

Components

GET/api/v1/pages/{pageId}/componentsread

List a page's components, in display order.

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/components" \
  -H "Authorization: Bearer $BEACON_API_KEY"
POST/api/v1/pages/{pageId}/componentswrite

Create a component on a page.

Body: name (required), description, parentId, position

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/components" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"API"}'
PATCH/api/v1/components/{id}write

Update a component, including its status.

Body: name, description, parentId, position, status, showUptime — at least one

curl -sS "$BEACON_URL/api/v1/components/$COMPONENT_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -X PATCH -d '{"status":"degraded"}'
DELETE/api/v1/components/{id}write

Delete a component. Its children (if any) are promoted to its own parent.

curl -sS "$BEACON_URL/api/v1/components/$COMPONENT_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -X DELETE
GET/api/v1/components/{id}/uptimeread

Daily uptime series for a component, aggregated across its monitors, plus a summary (overall % and worst day) over the window.

Query: days (1-365, default 90), or start & end (YYYY-MM-DD, capped at 365 days)

curl -sS "$BEACON_URL/api/v1/components/$COMPONENT_ID/uptime?start=2026-06-01&end=2026-06-30" \
  -H "Authorization: Bearer $BEACON_API_KEY"

Incidents & maintenance

GET/api/v1/pages/{pageId}/incidentsread

List a page's incidents and maintenances, newest first. q searches the title, every update body and the published postmortem.

Query: q, status, kind (each comma-separated), include_drafts (default false), limit (1-500, default 50)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/incidents?kind=incident&status=investigating,identified" \
  -H "Authorization: Bearer $BEACON_API_KEY"
GET/api/v1/pages/{pageId}/incidents/unresolvedread

Incidents that are still open — kind incident, any status but resolved.

Query: limit (1-500, default 50)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/incidents/unresolved" \
  -H "Authorization: Bearer $BEACON_API_KEY"
GET/api/v1/pages/{pageId}/incidents/scheduledread

Maintenance windows announced but not started (status scheduled), soonest first.

Query: limit (1-500, default 50)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/incidents/scheduled" \
  -H "Authorization: Bearer $BEACON_API_KEY"
GET/api/v1/pages/{pageId}/incidents/upcomingread

Scheduled maintenance whose window has not begun yet, soonest first.

Query: limit (1-500, default 50)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/incidents/upcoming" \
  -H "Authorization: Bearer $BEACON_API_KEY"
GET/api/v1/pages/{pageId}/incidents/active_maintenanceread

Maintenance windows happening right now (status in_progress).

Query: limit (1-500, default 50)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/incidents/active_maintenance" \
  -H "Authorization: Bearer $BEACON_API_KEY"
POST/api/v1/pages/{pageId}/incidentswrite

Open an incident or maintenance. Supports "Idempotency-Key" — see below.

Body: title, body (required); kind, status, severity, componentIds, componentImpact, scheduledStart, scheduledEnd, metadata, publish

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/incidents" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f2a9c9e-2b41-4c9a-9c2e-1b1e6a2b9d10" \
  -d '{
        "title": "Elevated API error rates",
        "body": "We are investigating elevated 500s on the API.",
        "severity": "major",
        "componentIds": ["'"$COMPONENT_ID"'"]
      }'
GET/api/v1/incidents/{id}read

Fetch one incident (or maintenance) with its full update timeline.

curl -sS "$BEACON_URL/api/v1/incidents/$INCIDENT_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY"
PATCH/api/v1/incidents/{id}write

Correct an incident's title, severity or metadata. A correction, not a lifecycle step: it writes no timeline entry and notifies nobody. metadata merges; a key set to null is removed.

Body: title, severity (null clears it), metadata — at least one

curl -sS "$BEACON_URL/api/v1/incidents/$INCIDENT_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -X PATCH -d '{"severity":"critical","metadata":{"jiraKey":"OPS-4412"}}'
DELETE/api/v1/incidents/{id}write

Delete an incident and its updates. Permanent, and it disappears from the public history — prefer resolving anything customers already saw.

curl -sS "$BEACON_URL/api/v1/incidents/$INCIDENT_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -X DELETE
POST/api/v1/incidents/{id}/updateswrite

Post a follow-up update and move the incident to a new status.

Body: status, body (required); componentStatuses, publish

curl -sS "$BEACON_URL/api/v1/incidents/$INCIDENT_ID/updates" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"status":"monitoring","body":"A fix has been deployed; we are watching error rates."}'
POST/api/v1/incidents/{id}/resolvewrite

Close an incident (or complete a maintenance) with a final update. Shorthand for posting the terminal status.

Body: body (required)

curl -sS "$BEACON_URL/api/v1/incidents/$INCIDENT_ID/resolve" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"body":"The fix has been verified in production; error rates are back to baseline."}'
PATCH/api/v1/incidents/{id}/updates/{updateId}write

Correct one already-posted update. Re-renders the page and re-runs translation, but never re-notifies subscribers — a typo fix must not mail everyone twice.

Body: body, displayAt (null clears the backdating override) — at least one

curl -sS "$BEACON_URL/api/v1/incidents/$INCIDENT_ID/updates/$UPDATE_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -X PATCH -d '{"body":"A fix has been deployed; error rates are back to baseline."}'

Subscribers

GET/api/v1/pages/{pageId}/subscribersread

List a page's subscribers, newest first, 50 per page. Targets come back in full (the dashboard table masks them).

Query: q, channel (email | slack | teams | discord | sms), page (1-based)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/subscribers?channel=email" \
  -H "Authorization: Bearer $BEACON_API_KEY"
POST/api/v1/pages/{pageId}/subscriberswrite

Add a subscriber by hand. email starts unverified and triggers the same double opt-in mail the public form sends; webhook channels and sms are verified immediately.

Body: channel, target (both required)

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/subscribers" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel":"email","target":"ops@example.com"}'
DELETE/api/v1/subscribers/{id}write

Remove one subscriber. This is the operator-side removal — a recipient's own unsubscribe is the signed link in their notifications and needs no key.

curl -sS "$BEACON_URL/api/v1/subscribers/$SUBSCRIBER_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -X DELETE

Metrics

GET/api/v1/pages/{pageId}/metricsread

List a page's system metrics in display order, each with its latest value, when that value landed, and how many points arrived in the last 24 hours.

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/metrics" \
  -H "Authorization: Bearer $BEACON_API_KEY"
POST/api/v1/pages/{pageId}/metricswrite

Define a metric — the chart itself, not its data. Published metrics appear on the public status page; data arrives separately at the endpoints below. Capped by your plan's metric limit.

Body: name (required); suffix, decimals (0-6), published, position

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/metrics" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"API response time","suffix":"ms","decimals":0,"published":true}'
PATCH/api/v1/metrics/{id}write

Rename a metric, change its formatting, reorder it, or publish/unpublish it.

Body: name, suffix, decimals, published, position — at least one

curl -sS "$BEACON_URL/api/v1/metrics/$METRIC_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -X PATCH -d '{"published":true}'
DELETE/api/v1/metrics/{id}write

Delete a metric and every data point it holds. Permanent.

curl -sS "$BEACON_URL/api/v1/metrics/$METRIC_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -X DELETE
POST/api/v1/metrics/{id}/datawrite

Submit points for one metric. ts accepts epoch seconds, epoch milliseconds or ISO-8601, and defaults to the server clock. Points are bucketed to 30s (same bucket = last value wins); anything older than the retention window or more than a minute in the future is refused individually and counted in skipped.

Body: { value, ts? } for one sample, or { points: [{ ts?, value }, …] } for up to 3000

curl -sS "$BEACON_URL/api/v1/metrics/$METRIC_ID/data" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"value":141}'
POST/api/v1/pages/{pageId}/metrics/datawrite

Submit points for several metrics at once, keyed by metric id. Every id is checked against the page before anything is written and the whole body is one transaction — so a request naming one unknown metric writes nothing and 404s, which makes retrying the same body safe. The 3000-point cap counts across the whole body.

Body: { "<metricId>": [{ ts?, value }, …] } — a single object may stand in for a one-element array

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/metrics/data" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{"'"$METRIC_ID"'":[{"value":141},{"value":139}]}'

Incident templates

GET/api/v1/pages/{pageId}/templatesread

List a page's incident templates, by name.

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/templates" \
  -H "Authorization: Bearer $BEACON_API_KEY"
POST/api/v1/pages/{pageId}/templateswrite

Create a template. Inert data — creating one opens nothing and notifies nobody.

Body: name, title, body (required); kind, severity, componentIds, componentImpact

curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/templates" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -d '{
        "name": "Payments degraded",
        "title": "Payment processing is degraded",
        "body": "We are investigating elevated failures at our payment provider.",
        "severity": "major"
      }'
GET/api/v1/templates/{id}read

Fetch one template.

curl -sS "$BEACON_URL/api/v1/templates/$TEMPLATE_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY"
PATCH/api/v1/templates/{id}write

Update a template. The whole record is re-normalized against the resulting kind, so switching to maintenance drops a severity that no longer applies instead of erroring.

Body: name, kind, title, body, severity, componentIds, componentImpact — at least one

curl -sS "$BEACON_URL/api/v1/templates/$TEMPLATE_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -H "Content-Type: application/json" \
  -X PATCH -d '{"severity":"critical"}'
DELETE/api/v1/templates/{id}write

Delete a template. Incidents already opened from it are untouched.

curl -sS "$BEACON_URL/api/v1/templates/$TEMPLATE_ID" \
  -H "Authorization: Bearer $BEACON_API_KEY" -X DELETE

Component status values: operational, degraded, partial_outage, major_outage, maintenance.

Incident status values: investigating, identified, monitoring, resolved.

Maintenance status values: scheduled, in_progress, verifying, completed.

Incident severity values: none, minor, major, critical.

Subscriber channels: email, slack, teams, discord, sms.

Webhook signatures

Webhooks you configure under Settings → Webhooks receive a POST whenever a published incident update goes out, with headers (the X-Beacon- prefix is a legacy header name, kept unchanged so existing integrations keep verifying):

  • X-Beacon-Event — currently always incident.update.
  • X-Beacon-Timestamp — Unix seconds when the request was signed.
  • X-Beacon-Signature — sha256=HMAC_SHA256(secret, timestamp + "." + rawBody), hex-encoded.

Verify it against the endpoint's secret (shown once, at creation) using the exact raw request body — not a re-serialized copy — and a constant-time comparison:

const crypto = require("node:crypto");

function isValidBeaconWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-beacon-timestamp"];
  const signature = headers["x-beacon-signature"];
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The payload is JSON:

{
  "incidentId": "...",
  "updateId": "...",
  "title": "Elevated API error rates",
  "status": "monitoring",
  "severity": "major",
  "body": "A fix has been deployed; we are watching error rates.",
  "componentIds": ["..."],
  "pageUrl": "https://status.yourcompany.com",
  "updatedAt": "2026-08-29T18:04:00.000Z"
}

As with any webhook, treat a delivery as untrusted until its signature checks out, and reject requests whose X-Beacon-Timestamp is too old (a few minutes is a reasonable window) to guard against replay.

MCP

Prefer letting an agent operate IsItStillUp directly over calling this REST API from agent code? The same API keys authenticate two MCP (Model Context Protocol) servers:

  • /api/mcp — the authenticated admin server. The same operations as this REST API — list pages/components, open incidents, post updates, flip component status, schedule maintenance, read uptime — exposed as MCP tools instead of HTTP routes, with the same key and the same read/write scopes.
  • /s/{slug}/mcp — a read-only, unauthenticated server per status page, for answering "is it up right now?" from the page's published snapshot. No key required.

Both speak stateless MCP Streamable HTTP (JSON-RPC 2.0 over a single POST — no SSE, no session ids), so any MCP-capable client can point at either URL directly.

Depending on a third party’s MCP server rather than your own? See MCP status — a separate, unauthenticated server that measures and reports on public MCP endpoints.