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 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:
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 and uptime calendar.
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):
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
404for a deleted post, a402for an empty balance or a429from your own rate limit is not an outage. - A
503we 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?
uptime = 1 - (major minutes + 0.3 x partial minutes) / minutes with dataDays 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:
curl "https://www.socialcrawl.dev/v1/status"{
"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.
Subscribe to incidents
Use Subscribe on the status page 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.verificationevent that carries your signing secret insubscription.secret. Store the secret and answer with a2xx; 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.
{
"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=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256(secret, "<t>.<raw body>"). 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.
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);
}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:
{
"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.
curl -s "https://www.socialcrawl.dev/v1/status/youtube" \
| jq '{status: .platform.status, incidents: [.incidents[] | {id, status}]}'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<unknown> {
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");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.
