Public API
Every number on this site is public JSON: which providers are down right now, ninety days of uptime, every incident their own status pages published, and the crowd's click counter. It is the same data the pages are drawn from, read from the same cache.
Using it
- No key, no signup, no cookie. Every response carries Access-Control-Allow-Origin: *, so it works from a browser.
- Per IP: 300 reads a minute, and 10 clicks a second on POST /api/click. Over either is a 429 with Retry-After.
- Please say who you are in User-Agent, with a URL or an email. It is the only way to tell a broken script from a busy one.
- Live updates come over a WebSocket at /ws; polling GET /api/state every few seconds is the documented fallback.
- llms.txt is the same map for an answer engine, and the Atom feed carries sieges, incidents and Codex resets as they happen.
Every endpoint
| Method and path | Response | Cache-Control |
|---|---|---|
GET /api/state | same object as hello minus t/tickHz | public, max-age=1, stale-while-revalidate=2 + ETag: "ver-<ver>-<floor(all/1000)>", honours If-None-Match |
POST /api/click | ack object | no-store; 429 with Retry-After when the per-IP limiter (10 req/s) trips |
GET /api/history?provider=&limit=50&before=<iso> | {"items":[{"id","provider","startedAt","endedAt","durationS","clicks","spawns","breaches","peakZombies","wallsDown","wallTimes","zombiesV2","synthetic","incidents":[{"id","title","impact"}]}],"nextBef… | public, max-age=60 |
GET /api/sieges/{id} | siege as above + "events":[{"kind","at","value","payload"}] + "incidents":[{…,"updates":[{"status","body","createdAt"}]}] | max-age=300 if ended, max-age=5 if live |
GET /api/incidents?provider=&open=true&limit=50 | {"items":[{"id","provider","title","impact","status","startedAt","resolvedAt","url","updates":[…last 3…]}]} | public, max-age=30 |
GET /api/incidents/{provider}/{sourceId} | one incident's whole record (B29): {"id","provider","sourceId","title","impact","status","startedAt","resolvedAt","durationS","sourceUpdatedAt","lastSeenAt","url","statusUrl","updates":[{"status","bod… | public, max-age=30 while open, public, max-age=300 once resolved |
GET /api/resets?limit=50&before=<iso> | {"current":{"clicks":98765,"sinceAt":"<when the latest applied reset landed: effectiveAt when known, else announcedAt; appliedAt only when neither… | public, max-age=60 |
GET /api/stats | {"allTimeClicks","resetClicks","perProvider":{"<p>":{"sieges","totalClicks","totalBreaches","totalSiegeSeconds","currentPeaceStreakS","longestSiege":{"id","durationS"}}},"records":{"longestSiege":{"id… | public, max-age=60 |
GET /api/minutes?target=all|openai|anthropic|google|codex-reset&hours=24 | [{"minute":"<iso>","clicks","spawns","breaches"}] gap-filled with zeros | public, max-age=60 |
GET /api/status/{provider}?days=90 | one provider's own status page — {"provider","name","statusUrl","asOf","current":{"state":"ok|degraded|down","down","sinceS","openIncidents"},"windows":[{"window":"24h|7d|30d|<days>d","uptimePct","dow… | public, max-age=300 |
GET /api/uptime?days=90 | {"days":[{"date":"2026-09-03","openai":"ok|minor|major|critical","anthropic":"…","google":"…"}]} worst impact per UTC day | public, max-age=300 |
GET /feed.xml | Atom feed, 50 newest entries: siege started / siege ended / codex reset / incident opened | public, max-age=300 |
GET /og.png?p=all|openai|anthropic|google | 1200×630 PNG of the live state; ?v= is an ignored cache-buster | public, max-age=60 |
GET /badge.svg?provider=all|openai|anthropic|google|chatgpt|codex|claude|gemini | shields-style SVG, e.g. OpenAI · under siege · 7 zombies. The four product slugs (F31) label with the product and take their state from that product's own component subset, by the same filter and the… | public, max-age=60 |
GET /embed?p=all|openai|anthropic|google | embed.html (second Vite entry), header Content-Security-Policy: frame-ancestors * | no-cache |
GET / | index.html with injected <title>, meta description, og:, twitter: (og:image=/og.png?p=all&v=<ver>); the built index.html contains a single marker <!--mha:meta--> in <head> (no <title> of its own)… | no-cache |
GET /status/{chatgpt|codex|claude|gemini} | a product page: the parent provider's response narrowed to the components that product publishes, with its own title, h1, FAQ, four-item breadcrumb and a "Part of {Parent} status" line. It exists… | no-cache |
GET /status/{provider}/incidents/{sourceId} | an incident page (B29, R-521/R-522): breadcrumbs, <h1>{Provider} incident: {title}</h1>, an answer-first summary sentence, the timeline, the affected components, the siege the crowd fought, and the… | no-cache |
GET /v2 | the same shell, with <meta name="robots" content="noindex, follow"> and canonical = {base}/. It is a rendering variant of /, not a second page | no-cache |
GET /api-docs | the public API as a page (F33): the REST table and the WebSocket frames rendered from this file at build time, with a curl line per endpoint, the CORS and rate-limit statement, and the commit and… | no-cache |
GET /ads/terms | the advertising programme's three reader-facing pages (F35, R-525): the advertiser terms, the page PayPal returns an approving buyer to, and the page it returns a cancelling one to. Ordinary shell… | no-cache |
GET /robots.txt | served by the host, not a static file: Sitemap: line on the configured origin, /og.png explicitly allowed, /api/, /embed, /badge, /lab.html, /v2 disallowed, and an allow group for OAI-SearchBot,… | public, max-age=3600 |
GET /sitemap.xml | <urlset> of every indexable route with <lastmod> derived from data (newest incident, reset or siege for that page; omitted when the page has no data yet), <changefreq> and <priority>. Never lists… | public, max-age=300 |
GET /{indexnow-key}.txt | the IndexNow ownership proof: the key as text/plain. 404 when Mha__Seo__IndexNowKey is unset or the path is not the configured key | public, max-age=3600 |
GET /llms.txt | text/markdown: what the site is, every status and product page as a linked list, the public API behind them, and how to cite. The map an answer engine reads before it reads the pages | public, max-age=3600 |
GET /llms-full.txt | text/markdown: the same server-composed bodies as the pages, with the markup taken off (headings become ##, <dt>/<dd> merge onto one line, the breadcrumb <nav> is dropped as navigation rather than… | public, max-age=300 |
GET /humans.txt | text/plain: what the site is made of and who runs it. Names a maintainer only when Mha__Seo__MaintainerName is set; never an email, never anything identifying by default | public, max-age=3600 |
GET /.well-known/security.txt | text/plain per RFC 9116, with Contact, Expires (build stamp + 1 year, floored at now + 1 year) and Preferred-Languages. 404 when Mha__Seo__SecurityContact is unset — a security.txt with nowhere to… | public, max-age=3600 |
GET /feed.xml?p=openai|anthropic|google | the same Atom feed filtered to one provider; absent or all is the unfiltered feed | public, max-age=300 |
GET /assets/* | Vite hashed assets | public, max-age=31536000, immutable |
GET /healthz | see docs/architecture/deployment.md | no-store |
Built from docs/api.md 9bcacc78 on 2026-09-08.
Last checked .
Must Have AI is a parody and is not affiliated with, endorsed by or connected to OpenAI, Anthropic or Google. Every outage shown here is read from the provider's own public status page.