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:
| Plan | Price | Requests / minute |
|---|---|---|
| free | Free | 60 |
| starter | $5/mo | 600 |
| pro | $15/mo | 600 |
| business | $29/mo | 1,200 |
| enterprise | $99/mo | 3,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"
}
}| Status | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, or revoked API key. |
| 403 | forbidden | Valid key, but it lacks the "write" scope this endpoint needs. |
| 404 | not_found | The id doesn't exist, or doesn't belong to this key's organization. |
| 422 | validation_error | Malformed JSON, a failed schema check, or a rule @/lib/core rejected (e.g. a bad status transition). |
| 429 | rate_limited | This 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.
| Method | Path | Scope | Body / query |
|---|---|---|---|
| GET | /api/v1/pages | read | — |
| GET | /api/v1/pages/{pageId} | read | — |
| PATCH | /api/v1/pages/{pageId} | write | name, description, timeZone, hiddenFromSearch, subscriptionOptions, branding — at least one |
| GET | /api/v1/pages/{pageId}/components | read | — |
| POST | /api/v1/pages/{pageId}/components | write | name (required), description, parentId, position |
| PATCH | /api/v1/components/{id} | write | name, description, parentId, position, status, showUptime — at least one |
| DELETE | /api/v1/components/{id} | write | — |
| GET | /api/v1/components/{id}/uptime | read | days (1-365, default 90), or start & end (YYYY-MM-DD, capped at 365 days) |
| GET | /api/v1/pages/{pageId}/incidents | read | q, status, kind (each comma-separated), include_drafts (default false), limit (1-500, default 50) |
| GET | /api/v1/pages/{pageId}/incidents/unresolved | read | limit (1-500, default 50) |
| GET | /api/v1/pages/{pageId}/incidents/scheduled | read | limit (1-500, default 50) |
| GET | /api/v1/pages/{pageId}/incidents/upcoming | read | limit (1-500, default 50) |
| GET | /api/v1/pages/{pageId}/incidents/active_maintenance | read | limit (1-500, default 50) |
| POST | /api/v1/pages/{pageId}/incidents | write | title, body (required); kind, status, severity, componentIds, componentImpact, scheduledStart, scheduledEnd, metadata, publish |
| GET | /api/v1/incidents/{id} | read | — |
| PATCH | /api/v1/incidents/{id} | write | title, severity (null clears it), metadata — at least one |
| DELETE | /api/v1/incidents/{id} | write | — |
| POST | /api/v1/incidents/{id}/updates | write | status, body (required); componentStatuses, publish |
| POST | /api/v1/incidents/{id}/resolve | write | body (required) |
| PATCH | /api/v1/incidents/{id}/updates/{updateId} | write | body, displayAt (null clears the backdating override) — at least one |
| GET | /api/v1/pages/{pageId}/subscribers | read | q, channel (email | slack | teams | discord | sms), page (1-based) |
| POST | /api/v1/pages/{pageId}/subscribers | write | channel, target (both required) |
| DELETE | /api/v1/subscribers/{id} | write | — |
| GET | /api/v1/pages/{pageId}/metrics | read | — |
| POST | /api/v1/pages/{pageId}/metrics | write | name (required); suffix, decimals (0-6), published, position |
| PATCH | /api/v1/metrics/{id} | write | name, suffix, decimals, published, position — at least one |
| DELETE | /api/v1/metrics/{id} | write | — |
| POST | /api/v1/metrics/{id}/data | write | { value, ts? } for one sample, or { points: [{ ts?, value }, …] } for up to 3000 |
| POST | /api/v1/pages/{pageId}/metrics/data | write | { "<metricId>": [{ ts?, value }, …] } — a single object may stand in for a one-element array |
| GET | /api/v1/pages/{pageId}/templates | read | — |
| POST | /api/v1/pages/{pageId}/templates | write | name, title, body (required); kind, severity, componentIds, componentImpact |
| GET | /api/v1/templates/{id} | read | — |
| PATCH | /api/v1/templates/{id} | write | name, 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
List every status page this API key's organization owns.
curl -sS "$BEACON_URL/api/v1/pages" \
-H "Authorization: Bearer $BEACON_API_KEY"Fetch one status page and its settings.
curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID" \
-H "Authorization: Bearer $BEACON_API_KEY"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
List a page's components, in display order.
curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/components" \
-H "Authorization: Bearer $BEACON_API_KEY"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"}'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 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 DELETEDaily 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
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"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"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"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"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"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"'"]
}'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"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 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 DELETEPost 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."}'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."}'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
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"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"}'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 DELETEMetrics
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"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}'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 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 DELETESubmit 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}'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
List a page's incident templates, by name.
curl -sS "$BEACON_URL/api/v1/pages/$PAGE_ID/templates" \
-H "Authorization: Bearer $BEACON_API_KEY"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"
}'Fetch one template.
curl -sS "$BEACON_URL/api/v1/templates/$TEMPLATE_ID" \
-H "Authorization: Bearer $BEACON_API_KEY"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 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 DELETEComponent 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 alwaysincident.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.