Drıftstack docs
Docs

Status page API

The /v1/status/* surface backs the public Driftstack status site. It is intentionally unauthenticated — visitors don’t have accounts — and rate-limited per IP. Snapshot and incident responses are cacheable for 30 seconds (see the Cache-Control headers below).

Snapshot

GET /v1/status

Returns the current health snapshot — overall status, per-component breakdown, and the last 5 public incidents from the past 30 days.

Response (200):

{
  "overall_status": "operational",
  "components": [
    { "name": "postgres", "status": "operational", "last_checked_at": "<ISO-8601>" },
    { "name": "redis", "status": "operational", "last_checked_at": "<ISO-8601>" },
    { "name": "r2", "status": "operational", "last_checked_at": "<ISO-8601>" }
  ],
  "recent_incidents": [
    {
      "id": "inc_<uuid>",
      "title": "Elevated session-create latency",
      "severity": "minor",
      "status": "monitoring",
      "started_at": "<ISO-8601>",
      "resolved_at": null
    }
  ],
  "open_incidents": 0,
  "incident_data_complete": true
}

overall_status and per-component status are one of:

  • operational — the health check succeeded within its timeout
  • degraded — the health check failed (transient error or timeout)
  • major_outage — overall only: an open public incident of outage severity

A component is only ever operational or degraded. Overall: an open outage incident → major_outage; otherwise any degraded component, any open incident, or incident data that could not be read → degraded; otherwise operational. open_incidents is the number of open public incidents, whatever their age, or null when incident_data_complete is false (the incident data could not be read).

Cache-Control: public, max-age=30 — the snapshot may be served from cache for up to 30 seconds.

Incident feed

GET /v1/status/incidents

Lists every open public incident first, whatever its age, then resolved ones from the last 30 days (default), most-recent first. The status site renders this as the incident timeline.

Query parameters:

Parameter Required Notes
since optional ISO-8601 cutoff; defaults to 30 days ago
limit optional 1–100; defaults to 50
window optional 30d (default) or 90d, when since is absent

state and cursor are refused with 400 bad-request. since and window bound the resolved incidents only.

Response (200):

{
  "data": [
    {
      "id": "inc_<uuid>",
      "title": "<string>",
      "description": "<string>",
      "severity": "minor | major | outage",
      "status": "investigating | identified | monitoring | resolved",
      "affected_components": ["postgres", "redis"],
      "public": true,
      "started_at": "<ISO-8601>",
      "resolved_at": "<ISO-8601> | null",
      "created_at": "<ISO-8601>",
      "updated_at": "<ISO-8601>"
    }
  ],
  "total": 1,
  "open_count": 0,
  "open_outage_count": 0,
  "truncated": false
}

total counts every incident that matched (open plus resolved in the window), open_count and open_outage_count the open ones, and truncated is true when data holds fewer than total because of limit.

Cache-Control: public, max-age=30.

Incident detail

GET /v1/status/incidents/{id}

Returns the incident plus the full update timeline (investigation posted → identified → monitoring → resolved).

Response (200):

{
  "incident": {
    /* same shape as the list entry */
  },
  "updates": [
    {
      "id": "incu_<uuid>",
      "incident_id": "inc_<uuid>",
      "message": "<string>",
      "status": "investigating | identified | monitoring | resolved",
      "posted_at": "<ISO-8601>"
    }
  ]
}

Non-public incidents return 404 — the route deliberately returns the same shape as “incident doesn’t exist” so nobody can enumerate private incidents.

Live stream

GET /v1/status/stream

Server-Sent Events stream. Visitors with the status page open receive every incident.created and incident.resolved event in real time without needing to poll.

Event types emitted:

  • event: incident.created — fires when a new public incident is opened
  • event: incident.resolved — fires when a public incident transitions to resolved

data: payload (JSON) is the full event envelope: { event, generated_at, incident, update }, where event repeats the SSE event name (incident.created / incident.resolved), generated_at is an ISO timestamp, incident is the public incident object (same shape as GET /v1/status/incidents), and update is the incident update that triggered the event.

Heartbeat: a comment line is emitted every 30 seconds to keep the connection alive through proxies. Comments start with : and are ignored by EventSource clients per the SSE spec.

Example (TypeScript browser):

const stream = new EventSource('https://api.driftstack.dev/v1/status/stream');
stream.addEventListener('incident.created', (ev) => {
  const { generated_at, incident } = JSON.parse(ev.data);
  console.log(`[${generated_at}] new incident: ${incident.id} — ${incident.title}`);
});
stream.addEventListener('incident.resolved', (ev) => {
  const { incident } = JSON.parse(ev.data);
  console.log(`incident ${incident.id} resolved`);
});

Connection caps: the stream is public and unauthenticated, so it is bounded — 10 concurrent connections per IP and 500 in total. Past either, the request is refused with 503 feature-unavailable (Status stream at capacity; retry shortly.) and a Retry-After: 30 header, before the stream starts.

The per-IP figure is the one worth designing around: browsers open one connection per tab, and everyone behind a single office NAT or corporate proxy shares an address. An IPv6 caller is counted by its /64 network, so every device on one IPv6 network shares the 10. EventSource reconnects on its own and will keep retrying into the refusal, so a client that watches for the error event and backs off for the advertised 30 seconds recovers cleanly, while one that does not will loop.

SLA report

GET /v1/status/sla

Rolling 30-day uptime per monitored target, computed from Driftstack’s automated health checks.

Response (200):

{
  "data": [
    {
      "target": "<probe-target-name>",
      "uptimePct": 99.972,
      "totalProbes": 43200,
      "okCount": 43188,
      "failCount": 12,
      "lastProbeAt": "2026-08-11T14:59:00.000Z",
      "lastFailureAt": "2026-08-03T02:17:00.000Z",
      "windowStart": "2026-07-12T15:00:00.000Z",
      "windowEnd": "2026-08-11T15:00:00.000Z"
    }
  ]
}

While the health-probe service runs, each target is checked every 60 seconds, so a full 30-day window holds ~43,200 checks. uptimePct is okCount / totalProbes * 100 rounded to three decimal places, and is null for a target with no checks in the window: treat it as unknown, not as 100%. An empty data list means the service is not checking any target, so there is no uptime figure to read. lastFailureAt is null when the target has not failed inside the window. The window itself is reported as the windowStart/windowEnd timestamps rather than a day count.

Email subscriptions

People subscribe to incident emails on the status page itself: the form sends a confirmation link first (double opt-in), and every incident email carries a one-click unsubscribe link.

Errors

Status Type When
400 validation-failed query failed schema
404 not-found non-public incident id, OR malformed inc_<uuid> value
429 rate-limited a per-IP limit tripped: the snapshot, the SLA report, or the incident list or detail (60 a minute each)

Notes

  • Caching. /v1/status, /v1/status/incidents, the detail route and /v1/status/sla all send Cache-Control: public, max-age=30; the status site polls at that interval.
  • Stream auth. The stream endpoint is unauthenticated by design — there’s no per-customer access concept on the status page — and capped per IP as described above.
  • Component checks. Each component check runs with a 1.5 second timeout. A timeout counts as degraded, not major_outage.