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
}
]
}
overall_status and per-component status are one of:
operational— the health check succeeded within its timeoutdegraded— the health check failed (transient error or timeout)major_outage— a service-wide outage affecting multiple components
Aggregation: any major_outage → overall major_outage; otherwise
any degraded → overall degraded; otherwise operational.
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 public incidents 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 |
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>"
}
]
}
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 openedevent: incident.resolved— fires when a public incident transitions toresolved
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 | the snapshot or SLA report’s per-IP limit tripped |
Notes
- Caching.
/v1/status,/v1/status/incidents, and the detail route all sendCache-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, notmajor_outage.