# Status (/docs/status) How the status page decides each platform and endpoint state, the status endpoints and feeds, subscriptions, and what an agent should do on a 5xx The [status page](/status) shows the current state and 90 days of history for the API, every platform and every endpoint, plus every incident with its timeline. The same data is free to read from code, with no API key and no credit cost: ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/status/youtube" ``` That call returns YouTube's state, each active YouTube endpoint with its state and 90 daily cells, and any open incident that touches YouTube. The state is recomputed every minute from real customer calls and from our own test requests. Nothing on the page is typed in by hand unless an incident update says so. ## What does the status page show? - **A banner** with the overall state: all systems operational, some systems degraded, partial system outage, or major service outage. - **Active incidents** and scheduled maintenance, each with its updates. - **Components** in groups: Core (API, Website and dashboard, MCP server, Async jobs), then each platform. Expand a platform to see its endpoints. - **A 90-day bar** per component. Each cell is one UTC day; hover or focus it for uptime, request availability and that day's incidents. - **Past incidents** for the last 15 days, and the full [incident history](/status/history) and [uptime calendar](/status/uptime). History starts on the day the status page launched. Earlier days show as no data, never as green. ## How is a state decided? Each endpoint is judged on one number, the success rate of real calls (the SLI): ```text title="Formula" SLI = successful calls / (successful calls + 5xx calls) ``` - Only uncached calls count. A cache hit says nothing about the platform today. - Our own test traffic and internal accounts are left out. - A 4xx never counts on either side. A `404` for a deleted post, a `402` for an empty balance or a `429` from your own rate limit is not an outage. - A `503` we return by design (an endpoint that has been withdrawn, a region a platform does not serve) is not counted. Strong evidence makes an endpoint red on its own: in the last 15 minutes at least 10 calls, at least half of them 5xx, from at least 2 different customers, with no single customer holding 80% of the failures. At 90% failures it is a major outage, below that a partial outage. Weaker evidence (a few failures, or failures with no success for an hour) shows the endpoint as **Elevated error rate** and sends a **referee canary**: our own test request to that endpoint. Two failed canaries in a row confirm the outage. A passing canary clears it. This is why one caller with a bad input cannot turn a platform red. A state worsens after the evidence holds for 3 minutes and improves after 5. Both ends are backdated, so a recorded outage starts at the first failing minute and ends at the last one. An outage never fades to grey: it stays red until a real call or a canary succeeds. A platform is a partial outage when any endpoint is partial or major, and a major outage when every endpoint with traffic is. The banner reads Core and platforms only, never single endpoints. ## What do the states mean? | State | Shown as | Counts as downtime | | -------------------- | -------------------- | ---------------------- | | `operational` | Operational | No | | `degraded_performance` | Elevated error rate | No | | `partial_outage` | Partial outage | Yes, weighted 0.3 | | `major_outage` | Major outage | Yes, in full | | `under_maintenance` | Under maintenance | No | | `no_data` | No recent traffic | Left out of the day | A component with no calls and no checks for 60 minutes shows **No recent traffic**. That is not a failure, and those minutes are left out of uptime instead of counting as up. ## How is uptime calculated? ```text title="Formula" uptime = 1 - (major minutes + 0.3 x partial minutes) / minutes with data ``` Days are UTC. Elevated error rate, maintenance and no-data minutes are not downtime. The day colour follows the weighted downtime minutes (`major + 0.3 x partial`): green at 0, yellow up to 20, orange up to 60, red above 60, grey with no data, blue for maintenance only. The hover also shows request availability, which is the SLI for that day. An incident opens on its own once a platform has been at partial outage or worse for 30 minutes (5 minutes for a Core component). It moves to Monitoring when the components recover and to Resolved after 30 clear minutes, with the outage duration in the last update. An Elevated error rate never opens an incident. ## Status endpoints Both endpoints are public: no API key, 0 credits, no call to any platform. `GET /v1/status` lists only the platforms with a problem right now, as `degraded` or `down`, with their failing endpoints and the open incidents: ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/status" ``` ```json title="Response" { "status": "degraded", "platforms": { "youtube": { "status": "down", "endpoints": [{ "method": "GET", "path": "/v1/youtube/channel", "status": "down" }] } }, "upstreamVersions": { "v1": 0, "v2": 0, "v3": 0 }, "updated_at": "2026-10-20T12:00:00.000Z", "incidents": [ { "id": "k3v9w2m8q1zt", "title": "Elevated request failures on YouTube", "status": "investigating", "impact": "major", "url": "https://www.socialcrawl.dev/status/incidents/k3v9w2m8q1zt", "components": ["platform:youtube", "endpoint:GET youtube/channel"] } ], "status_page": "https://www.socialcrawl.dev/status" } ``` `GET /v1/status/{platform}` returns one platform in full: every active endpoint with its state (`operational`, `degraded_performance`, `partial_outage`, `major_outage`, `under_maintenance`, `no_data`) and 90 daily cells. An unknown platform is a `404`. If status data cannot be read, the answer is a `503` with `Retry-After: 60`, never a guessed state. ## Feeds and badges The status page publishes the same data in standard formats. All are public and CORS-enabled, and are cached for 30 seconds. | URL | Format | | --- | --- | | `https://www.socialcrawl.dev/status/api/v2/summary.json` | Statuspage v2 summary: page, overall status, components, open incidents | | `https://www.socialcrawl.dev/status/api/v2/status.json` | Overall indicator only (`none`, `minor`, `major`, `critical`) | | `/status/api/v2/components.json`, `incidents.json`, `incidents/unresolved.json` | Statuspage v2 components and incidents | | `/status/api/v2/scheduled-maintenances.json`, `scheduled-maintenances/upcoming.json`, `scheduled-maintenances/active.json` | Statuspage v2 maintenance | | `https://www.socialcrawl.dev/status/history.rss`, `history.atom` | The 25 most recent incidents | | `https://www.socialcrawl.dev/status/badge/{component}.svg` | A badge for `overall`, `api`, `website`, `mcp`, `async-jobs` or a platform slug | Because the JSON follows the Statuspage format, tools such as StatusGator and IsDown, and any Statuspage client, read it without changes. ```markdown title="Badge" ![SocialCrawl status](https://www.socialcrawl.dev/status/badge/youtube.svg) ``` ## Subscribe to incidents Use **Subscribe** on the [status page](/status) and choose a channel. You can follow everything or only the platforms you pick. - **Email.** Confirm the address from the email we send. Every email has a manage link and a one-click unsubscribe. - **Slack.** Paste a Slack incoming webhook URL (`hooks.slack.com`). A test message confirms it. - **Webhook.** Give an HTTPS URL. We send it a signed `subscription.verification` event that carries your signing secret in `subscription.secret`. Store the secret and answer with a `2xx`; the subscription is active only after that answer. - **RSS or Atom.** Add the feed URL above to any reader. No signup. You hear about an incident when it opens, on each update a person writes, and when it resolves. Automatic Monitoring updates show on the page only. ### Webhook events Events are `incident.created`, `incident.updated`, `incident.resolved`, `maintenance.scheduled`, `maintenance.started` and `maintenance.completed`. The event name is also in the `x-socialcrawl-event` header. ```json title="Response" { "event": "incident.created", "meta": { "unsubscribe": "https://www.socialcrawl.dev/api/status/unsubscribe?token=...", "documentation": "https://www.socialcrawl.dev/docs/status" }, "page": { "id": "socialcrawl", "status_indicator": "minor", "status_description": "Minor Service Outage" }, "incident": { "id": "k3v9w2m8q1zt", "kind": "incident", "name": "Elevated request failures on YouTube", "status": "investigating", "impact": "major", "shortlink": "https://www.socialcrawl.dev/status/incidents/k3v9w2m8q1zt", "started_at": "2026-10-20T11:00:00.000Z", "incident_updates": [ { "status": "investigating", "body": "Requests to 1 YouTube endpoint are failing at a higher rate than normal: ...", "affected_components": [{ "code": "endpoint:GET youtube/search/advanced", "new_status": "major_outage" }] } ], "components": [{ "id": "c20fc9b5072f", "code": "platform:youtube", "name": "YouTube" }] } } ``` A failed delivery is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 4 hours. A `410` response unsubscribes the URL. ### Verify the signature Every webhook carries `x-socialcrawl-signature: t=,v1=`. `v1` is `HMAC-SHA256(secret, ".")`. Verify over the raw body before you parse it, and reject a `t` more than 300 seconds from your clock. This is the same scheme as [Monitors webhooks](/docs/webhooks.md). ```typescript title="TypeScript" import crypto from "node:crypto"; export function verifyStatusWebhook(rawBody: string, header: string, secret: string): boolean { const parts = Object.fromEntries( header.split(",").map((kv) => [kv.slice(0, kv.indexOf("=")), kv.slice(kv.indexOf("=") + 1)]), ); const t = Number(parts.t); if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const a = Buffer.from(parts.v1 ?? "", "hex"); const b = Buffer.from(expected, "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python title="Python" import hashlib, hmac, time def verify_status_webhook(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(kv.split("=", 1) for kv in header.split(",")) try: t = int(parts["t"]) except (KeyError, ValueError): return False if abs(int(time.time()) - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(parts.get("v1", ""), expected) ``` ## What should an agent do on a 5xx? Every `500`, `502`, `503` and `504` body carries `error.status_url`, the status page filtered to the platform. When an open public incident explains the failure, it also carries `error.incident`: ```json title="Response" { "success": false, "error": { "type": "SERVICE_UNAVAILABLE", "message": "youtube is temporarily unavailable. Your credits have been refunded.", "status": 503, "doc_url": "https://www.socialcrawl.dev/docs/errors#service-unavailable", "retryable": true, "status_url": "https://www.socialcrawl.dev/status?platform=youtube", "incident": { "id": "k3v9w2m8q1zt", "title": "Elevated request failures on YouTube", "status": "investigating", "impact": "major", "url": "https://www.socialcrawl.dev/status/incidents/k3v9w2m8q1zt" } }, "credits_used": 0, "credits_remaining": 1240, "request_id": "req-abc123" } ``` The rule: if `error.incident` is present, stop retrying that platform. Poll `GET /v1/status/{platform}` (free) every few minutes and resume once the incident is gone. Without an incident, the failure is probably transient: honour `Retry-After` and use the backoff in [Error handling](/docs/errors.md#handling-retries). ```bash title="cURL" curl -s "https://www.socialcrawl.dev/v1/status/youtube" \ | jq '{status: .platform.status, incidents: [.incidents[] | {id, status}]}' ``` ```typescript title="TypeScript" const BASE = "https://www.socialcrawl.dev/v1"; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function callWithStatus(path: string, platform: string): Promise { for (let attempt = 0; attempt < 6; attempt++) { const res = await fetch(`${BASE}${path}`, { headers: { "x-api-key": "YOUR_API_KEY" } }); const body = await res.json(); if (res.ok) return body; if (res.status < 500) throw new Error(`${body.error.type}: ${body.error.message}`); if (body.error.incident) { // A known incident: wait for it to clear instead of retrying. for (;;) { await sleep(5 * 60_000); const status = await (await fetch(`${BASE}/status/${platform}`)).json(); if (!status.incidents?.some((i: { id: string }) => i.id === body.error.incident.id)) break; } continue; } const retryAfter = Number(res.headers.get("retry-after") ?? 2 ** attempt); await sleep(retryAfter * 1000); } throw new Error("Still failing after 6 attempts"); } await callWithStatus("/youtube/channel?handle=MrBeast", "youtube"); ``` ```python title="Python" import time import requests BASE = "https://www.socialcrawl.dev/v1" def call_with_status(path: str, platform: str, params: dict): for attempt in range(6): res = requests.get(f"{BASE}{path}", params=params, headers={"x-api-key": "YOUR_API_KEY"}) body = res.json() if res.ok: return body if res.status_code < 500: raise RuntimeError(f"{body['error']['type']}: {body['error']['message']}") incident = body["error"].get("incident") if incident: # A known incident: wait for it to clear instead of retrying. while True: time.sleep(300) status = requests.get(f"{BASE}/status/{platform}").json() if not any(i["id"] == incident["id"] for i in status.get("incidents", [])): break continue time.sleep(int(res.headers.get("Retry-After", 2 ** attempt))) raise RuntimeError("Still failing after 6 attempts") call_with_status("/youtube/channel", "youtube", {"handle": "MrBeast"}) ``` Failed requests are refunded automatically, so waiting costs nothing. `credits_remaining` on each response shows your balance. ## Next steps - [Error handling](/docs/errors.md): Every error code, which ones to retry, and backoff loops. - [Status page](/status): The current state of every platform and endpoint. - [Production checklist](/docs/production-checklist.md): Retries, idempotency and monitoring before you launch.