{"openapi":"3.1.0","info":{"title":"Status State API","version":"1.0.0","description":"Check whether a service is up and which parts are affected. Basic status summaries are free. Detailed status reports and recent change history cost USDC through x402 v2; no API key or signup is needed. The API guidance below explains what's included. See /.well-known/agent.json and /.well-known/x402 for current prices and supported payment networks.","x-guidance":"Status State API reports the live operational status of 33 developer-infrastructure vendors (cloud, AI APIs, auth, payments, and agentic trading venues).\n\nThis API does not answer pricing ladders, release or breaking-change history, or compute availability - those are separate products.\n\nStart with the free GET /api/monitors: it returns every tracked vendor with its slug and current overall status. Use a slug from that catalog on the paid routes - GET /api/monitors/{slug} for the vendor's component status plus the incident it is currently working, if any and GET /api/monitors/{slug}/changes for up to 20 most recent real status changes, newest first.\n\nPaid routes cost $0.01 per call in USDC on Base or Solana via x402 (eip155:8453 or solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp), settled through the PayAI facilitator on every one of them. There is no API key and no signup: call the route, read the 402 challenge, pick whichever accept entry matches the chain your wallet holds USDC on, pay it, and retry with the PAYMENT-SIGNATURE header. One call is one payment on one chain; the challenge lists the alternatives, it does not charge for them. The slug is checked against the free catalog before the challenge is issued, so an untracked slug 404s immediately with no payment challenge at all - take slugs from GET /api/monitors and you will never see it.\n\nGET /api/site/status/{slug} is a free single-vendor teaser (slug, name, overall status) and GET /api/healthz reports service and payment-subsystem health. Statuses are normalized to one of: operational, degraded, partial_outage, major_outage, maintenance, unknown.","x-logo":{"url":"https://status-state-api.replit.app/logo.png","altText":"Status State API"},"contact":{"email":"statusstateapi@proton.me","url":"https://github.com/xs10chill"}},"servers":[{"url":"https://status-state-api.replit.app","description":"This API's own origin"}],"tags":[{"name":"health","description":"Health and payment-subsystem status"},{"name":"monitors","description":"Vendor status monitors (free summaries + paid full detail)"}],"paths":{"/api/healthz":{"get":{"operationId":"healthCheck","tags":["health"],"summary":"Health check","description":"Returns server health, DB connectivity, and payment subsystem status.\nA degraded database answers 200 with `status: degraded` rather than\nfailing, so this route only 503s while the process is being replaced.\n","responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"security":[]}},"/api/monitors":{"get":{"operationId":"listMonitors","tags":["monitors"],"summary":"List all monitors (free)","description":"Free endpoint. Returns every tracked vendor with its current\noverall status only - no component breakdown or incident detail.\nUse GET /monitors/{slug} (paid) for component status and the\nvendor's current incident.\n\nAnswers are served from a short-lived cache of the last successful\nread, and keep being served from it while the database is\nunreachable, so a transient failure degrades to slightly older status\nrather than an error. Read `Age` and `X-Catalog-Cache` to tell how\nold an answer is and why; `lastCheckedAt` is never re-stamped.\n\nScan the free catalog, and pay GET /api/monitors/{slug} only when overallStatus is not operational or hasActiveIncident is true. The component list and incident body still cost 0.01 USDC. No single slug is the product.\n","responses":{"200":{"description":"List of monitor summaries","headers":{"Age":{"$ref":"#/components/headers/Age"},"X-Catalog-Cache":{"$ref":"#/components/headers/CatalogCache"}},"content":{"application/json":{"schema":{"type":"object","required":["monitors"],"properties":{"monitors":{"type":"array","items":{"$ref":"#/components/schemas/MonitorSummary"}}}}}}},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"security":[]}},"/api/site/status/{slug}":{"get":{"operationId":"getSiteStatus","tags":["monitors"],"summary":"Minimal status teaser (free)","description":"Free single-vendor teaser. Returns { slug, name, homepageUrl,\noverallStatus, lastCheckedAt } - the same MonitorSummary shape\nGET /monitors lists, for one vendor. No component breakdown and no\nincident detail; that is GET /monitors/{slug} (paid). Returns 404\nfor an unknown slug (no payment is ever involved on this route).\n\nWhen the vendor's own row cannot be read, this falls back to the\nsame cached catalog GET /monitors serves, so a transient database\nfailure degrades to slightly older status rather than an error. Only\nthose degraded answers carry `Age` and `X-Catalog-Cache`; a fresh\nread carries neither.\n\nScan the free catalog, and pay GET /api/monitors/{slug} only when overallStatus is not operational or hasActiveIncident is true. The component list and incident body still cost 0.01 USDC. No single slug is the product.\n","parameters":[{"$ref":"#/components/parameters/MonitorSlug"}],"responses":{"200":{"description":"Minimal status","headers":{"Age":{"$ref":"#/components/headers/Age"},"X-Catalog-Cache":{"$ref":"#/components/headers/CatalogCache"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorSummary"}}}},"404":{"description":"Unknown monitor slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"security":[]}},"/api/monitors/{slug}":{"get":{"operationId":"getMonitorDetail","tags":["monitors"],"summary":"Monitor snapshot (paid)","description":"Paid via x402 (see /.well-known/x402). Returns the vendor's\nnormalized state: overall status, component status, and the\nincident the vendor is currently working, if any.\n\nThe component list and the incident are projected for a caller\nmaking a decision rather than mirrored from the vendor's page -\nevery non-operational component is reported, the operational tail\nis capped and counted, and an incident the vendor abandoned months\nago is reported as old rather than as current. The `MonitorDetail`\nschema states both rules exactly.\n\nThe slug is checked against the free GET /monitors catalog before\nthe payment gate, so an unknown one 404s immediately, with no\npayment challenge and nothing to pay. See the `slug` parameter for\nthe one case that still reaches the challenge undecided.\n","parameters":[{"$ref":"#/components/parameters/MonitorSlug"}],"responses":{"200":{"description":"Monitor snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorDetail"}}}},"402":{"description":"Payment required (x402 challenge): $0.01 in USDC on Base or Solana, one accept entry per chain, every one of them verified and settled through the PayAI facilitator. Pay whichever chain your wallet holds USDC on; one call is charged once."},"404":{"description":"Unknown monitor slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"$ref":"#/components/responses/PaidDataUnavailable"}},"x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0.01"},"protocols":[{"x402":{"networks":[{"network":"eip155:8453","name":"Base","payTo":"0xa039e0e59a7a78c36277cd9fcdda4e4f9d4467d4","facilitator":"https://facilitator.payai.network"},{"network":"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp","name":"Solana","payTo":"7o6ijXGr4YnPzsLMGiAJQ7sGCWg9t9LnpoSrAoMKJWce","facilitator":"https://facilitator.payai.network"}]}}]}}},"/api/monitors/{slug}/changes":{"get":{"operationId":"getMonitorChanges","tags":["monitors"],"summary":"Change history (paid)","description":"Paid via x402. Returns the most recent detected real status changes\nfor a monitor (up to 20, newest first). Extraction-quality\nimprovements never appear here - only changes the fingerprinting\nlogic judged to be real vendor-side changes.\n","parameters":[{"$ref":"#/components/parameters/MonitorSlug"}],"responses":{"200":{"description":"Change history","content":{"application/json":{"schema":{"type":"object","required":["slug","changes","staleness"],"properties":{"slug":{"type":"string"},"changes":{"type":"array","items":{"$ref":"#/components/schemas/ChangeRecord"}},"staleness":{"$ref":"#/components/schemas/Staleness"}}}}}},"402":{"description":"Payment required (x402 challenge): $0.01 in USDC on Base or Solana, one accept entry per chain, every one of them verified and settled through the PayAI facilitator. Pay whichever chain your wallet holds USDC on; one call is charged once."},"404":{"description":"Unknown monitor slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"$ref":"#/components/responses/PaidDataUnavailable"}},"x-payment-info":{"price":{"mode":"fixed","currency":"USD","amount":"0.01"},"protocols":[{"x402":{"networks":[{"network":"eip155:8453","name":"Base","payTo":"0xa039e0e59a7a78c36277cd9fcdda4e4f9d4467d4","facilitator":"https://facilitator.payai.network"},{"network":"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp","name":"Solana","payTo":"7o6ijXGr4YnPzsLMGiAJQ7sGCWg9t9LnpoSrAoMKJWce","facilitator":"https://facilitator.payai.network"}]}}]}}}},"components":{"headers":{"Age":{"description":"Seconds since the database read this body was built from. `0` on a\nfresh read. The body is never re-stamped, so `lastCheckedAt` stays\nthe time the vendor was actually checked, whatever `Age` says.\n","schema":{"type":"integer","minimum":0}},"CatalogCache":{"description":"Why this body is the age it is. `miss`: read from the database for\nthis request. `hit`: reused from the short-lived cache. `stale-on-error`:\nthe read failed and the last successful one was served instead - it\nis real data that was true when it was read, just not re-verified.\n","schema":{"type":"string","enum":["hit","miss","stale-on-error"]}},"RetryAfter":{"description":"Seconds to wait before retrying. Always short: a 503 from this API\nmeans \"not now\", never \"gone\".\n","schema":{"type":"integer","minimum":0}}},"responses":{"PaidDataUnavailable":{"description":"Returned *instead of* the 402 challenge, so nothing is ever charged\nfor it. Three causes, all transient and all safe to retry after\n`Retry-After` seconds:\n\n- this slug has never been checked at all, so there is no snapshot\n  to sell or to label as stale. `staleSeconds` is null when this is\n  why. Once a monitor has any recorded data, age no longer refuses\n  the request - it is reported instead via the `staleness` field on\n  the 200 response.\n- the process is being replaced by a redeploy and is turning away new\n  requests.\n- a dependency needed to answer is momentarily unavailable.\n\nThis refusal applies only to a slug that exists: an unknown slug\nnever reaches this far, since it already 404d against the free\ncatalog before the payment gate. A malformed or placeholder slug\nstill receives the 402 challenge, so the price stays discoverable\nto an agent directory probing the raw path template.\n","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaleDataError"}}}},"ServiceUnavailable":{"description":"Transient: the answer is not available *right now*. Two causes, both\nshort-lived and both safe to retry after `Retry-After` seconds - the\nprocess is being replaced by a redeploy and is turning away new\nrequests, or a free route was asked for data before it had ever\nsuccessfully read any (a cold start during a database failure).\nNothing is charged: on paid routes this is returned instead of the\npayment challenge, so a caller can never pay for a 503.\n","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"parameters":{"MonitorSlug":{"name":"slug","in":"path","required":true,"description":"Vendor slug from the free GET /api/monitors catalog. On the paid\nroutes an unknown but well-formed slug is checked against that same\ncatalog ahead of the payment gate and 404s immediately, with no\npayment challenge at all - the same way the free route 404s an\nunknown slug. A malformed slug (including the literal `{slug}`\ntemplate an agent directory probes with) is a different case: it\ndeliberately still reaches the 402 challenge, so the price stays\ndiscoverable even for that probe, and only 400s once a payment has\nbeen presented (or not); settlement is cancelled for any response\n>= 400, so a malformed slug is never charged either.\n","schema":{"type":"string","pattern":"^[a-z0-9-]{1,64}$","example":"openai"},"example":"openai"}},"schemas":{"StaleDataError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"slug":{"type":"string"},"staleSeconds":{"type":"number","nullable":true,"description":"Age of the last check attempt for this slug. Always null here:\nthis error now fires only when the slug has never been checked\nat all, so there is no age to report. Once a monitor has any\nrecorded data, its paid routes no longer refuse for staleness -\nsee the `staleness` field on MonitorDetail and on the change\nhistory response instead.\n"},"freshnessLimitSeconds":{"type":"number","description":"The age at which this slug's data would be considered stale:\ntwice its check interval, floored at 10 minutes.\n"},"retryAfterSeconds":{"type":"number"}}},"Staleness":{"type":"object","required":["isStale","staleSeconds","freshnessLimitSeconds"],"description":"Whether the data alongside this field is past its freshness\nwindow - the same computation that used to mean a 503 instead of a\nbody. Paid routes now sell stale data labeled rather than refusing\nit, so a caller that wants the freshest possible answer can check\n`isStale` itself instead of the request failing. A 503\n(StaleDataError) is still returned when nothing has ever been\nrecorded for the slug, since there is nothing to label in that case.\n","properties":{"isStale":{"type":"boolean"},"staleSeconds":{"type":"number","nullable":true,"description":"Seconds since the last check attempt. Only null in the\ndegenerate case where this shape is built for a monitor with no\ncheck attempt at all - every real paid response reaching this\nfield has already passed that check upstream.\n"},"freshnessLimitSeconds":{"type":"number","description":"The age at which this monitor's data is considered stale: twice\nits check interval, floored at 10 minutes.\n"}}},"HealthStatus":{"type":"object","required":["status","database","monitorCount","erroringCount","payments"],"properties":{"status":{"type":"string","enum":["ok","degraded"]},"database":{"type":"string","enum":["ok","error"]},"monitorCount":{"type":"number","nullable":true,"description":"Enabled monitors in the catalog, or null if the count query failed"},"erroringCount":{"type":"number","nullable":true,"description":"Monitors whose last check recorded an error, or null if the count query failed"},"lastError":{"type":"string","description":"The most recent monitor error, prefixed with its slug. Present\nonly when `erroringCount` is above zero, so a non-zero count\nalways comes with something concrete to look at.\n"},"payments":{"type":"object","required":["configured","facilitatorReachable"],"properties":{"configured":{"type":"boolean"},"facilitatorReachable":{"type":"boolean","nullable":true,"description":"Result of the last real facilitator probe: a `/verify` call\nwith a deliberately invalid payment that the facilitator must\nreject *for the right reason*. `true` therefore means the\nwhole payment path parsed - scheme, network, asset and payee -\nnot merely that the host answered. `null` means no probe has\ncompleted yet. Measured on a cadence and read from memory, so\npolling this endpoint never calls the facilitator.\n"},"network":{"type":"string"},"lastProbedAt":{"type":"string","format":"date-time","nullable":true,"description":"When that probe ran. Survives a restart, so it may predate\n`process.startedAt` - the facilitator did not restart just\nbecause this service did.\n"},"lastProbeDetail":{"type":"string","nullable":true,"description":"Short outcome, usually the facilitator's own reject reason."}}},"process":{"type":"object","description":"This process's own liveness.","properties":{"pid":{"type":"number"},"startedAt":{"type":"string","format":"date-time","nullable":true},"heartbeatAt":{"type":"string","format":"date-time","nullable":true,"description":"Last persisted proof of life, rewritten every 30s. The gap\nbetween this and the next boot is what outage detection reads.\n"}}},"scheduler":{"type":"object","description":"Whether checks are still being *made*. This is the part of health\nthat `erroringCount` cannot express: when the sweep stalls, no\ncheck runs, nothing fails, and the error count falls to zero. A\nzero error count is not evidence of health on its own.\n","properties":{"lastCycleAt":{"type":"string","format":"date-time","nullable":true,"description":"End of the last completed sweep *in this process*. Null until\none finishes; never inherited from a previous process, so it\ncan't describe a sweep loop that no longer exists.\n"},"lastCycleReason":{"type":"string","nullable":true,"enum":["ok","timed_out","aborted","latched",null]},"stale":{"type":"boolean","description":"No sweep has completed within the allowed window, measured\nfrom process start when none has completed at all.\n"},"maxSnapshotAgeSeconds":{"type":"number","nullable":true,"description":"Age of the oldest *successful observation* across enabled\nmonitors.\n"},"maxCheckAgeSeconds":{"type":"number","nullable":true,"description":"Age of the oldest *attempt* across enabled monitors. This is\nthe clock the paid freshness gate enforces; it differs from\n`maxSnapshotAgeSeconds` for a monitor that is being checked\nand failing.\n"},"staleMonitorCount":{"type":"number","nullable":true},"enabledMonitorCount":{"type":"number","nullable":true}}},"outage":{"type":"object","nullable":true,"description":"The most recent reconstructed window in which this service was\nnot running, found by comparing the previous process's last\nheartbeat against this one's boot. Recorded after the fact - a\nprocess that is gone cannot report anything.\n","properties":{"startedAt":{"type":"string","format":"date-time"},"endedAt":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["unplanned","planned_republish"]},"downSeconds":{"type":"number"}}},"memory":{"type":"object","description":"Container memory as the OS/cgroup accounts it - what an OOM kill\nactually watches - not this process's own\n`process.memoryUsage().rss`, which misses memory used by\nsubprocesses (the headless browser behind rendered-page checks)\nand can read healthy while the container is minutes from being\nkilled.\n","properties":{"available":{"type":"boolean","description":"Whether a cgroup memory reading could be taken on this host.\nFalse on a platform with no accessible cgroup - the fields\nbelow are then meaningless, not zero.\n"},"usedBytes":{"type":"number","nullable":true},"limitBytes":{"type":"number","nullable":true,"description":"Null when the cgroup reports no limit."},"ratio":{"type":"number","nullable":true,"description":"usedBytes / limitBytes. Null when there is no limit to\ndivide by, or when a reading is unavailable.\n"},"level":{"type":"string","enum":["ok","warning","critical","unknown"],"description":"unknown when a reading is unavailable."},"processRssBytes":{"type":"number","description":"This process's own `process.memoryUsage().rss`, alongside\nthe cgroup figures above for contrast - the two can diverge\nsignificantly when a subprocess is using memory this\nprocess did not allocate itself.\n"}}}}},"StatusLevel":{"type":"string","enum":["operational","degraded","partial_outage","major_outage","maintenance","unknown"]},"MonitorSummary":{"type":"object","required":["slug","name","homepageUrl","overallStatus","lastCheckedAt"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"homepageUrl":{"type":"string"},"overallStatus":{"$ref":"#/components/schemas/StatusLevel"},"lastCheckedAt":{"type":"string","format":"date-time","nullable":true},"hasActiveIncident":{"type":"boolean","description":"True only when the paid GET /monitors/{slug} snapshot would carry\na non-null activeIncident. Same value the unpaid 402 body carries.\nOmitted, with the two counts, when the last good snapshot has no\ncomponent list.\n"},"componentCount":{"type":"number","description":"Size of the component roster the paid snapshot publishes (listed\nplus componentsOmitted). Omitted when the last good snapshot has\nno component list. Names are never included here.\n"},"notOperationalComponentCount":{"type":"number","description":"How many of those components are not operational. Maintenance is\nnever counted. Omitted when the last good snapshot has no\ncomponent list.\n"}}},"StatusComponent":{"type":"object","required":["name","status"],"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/StatusLevel"}}},"Incident":{"type":"object","required":["name","status"],"properties":{"name":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true},"url":{"type":"string","nullable":true},"latestUpdate":{"type":"string","nullable":true,"description":"The body of the vendor's most recent update on this incident,\nverbatim and unsummarized - where the vendor says what the\noutage actually means (\"trading is cancel-only\", \"reads are\nunaffected\") rather than what it is called. It may run to\nseveral lines; very long updates are truncated.\n\nAlways plain text. Several vendors compose these updates as\nHTML because their own status page renders them; markup is\nconverted to text (block elements become line breaks, entities\nare decoded) and never republished as tags. The words are the\nvendor's own and are not otherwise altered.\n\nNull whenever the vendor's source publishes no update text.\nSources that carry it are Atlassian Statuspage feeds and the\nshims that copy their schema, Status.io, Better Stack status\npages, and the AWS Service Health Dashboard. The remaining\nsources publish only an incident's title, status and\ntimestamps, and nothing is substituted for the missing prose -\nnot a summary of ours, and not text taken from somewhere on\nthe page the vendor did not publish as an update. Null also\ncovers the narrower cases where a supported source carries no\nusable text for the incident in front of it: an event with no\nupdates posted yet, or a Better Stack page with two reports\nopen at once, where the merged incident has no single author.\n\nThis is payload, not a change signal. Vendors edit a published\nupdate to fix wording without the situation moving, so a\nrewrite writes no row in `GET /api/monitors/{slug}/changes`.\nRead the current text here and date it with `sourceUpdatedAt`.\n"}}},"ComponentsOmitted":{"type":"object","required":["count","operational","maintenance","notOperational","summary"],"description":"What the component cap left out, and what the vendor's full roster\nlooks like. Present (non-null) only when `components` is shorter\nthan the roster the vendor publishes.\n\nThe cap treats a component's status as either a fault (degraded,\npartial outage, major outage, or unknown - counted in\n`notOperational`, always fully listed) or filler (operational or\nmaintenance - counted in `operational` / `maintenance`, capped).\n`maintenance` is filler on purpose: it is scheduled vendor work, not\na problem, and protecting it the way a real fault is protected would\nlet a vendor running one large maintenance window (Cloudflare\nposting `maintenance` on dozens of edge PoPs at once is the case\nthis was written for) sell as long a body as a vendor actually\nhaving an incident.\n","properties":{"count":{"type":"number","description":"How many components were left out of `components`. Every one of\nthem is filler - operational or under maintenance - never a\nfault: the list is built fault components first, and a filler\ncomponent is never kept in place of one that is actually\nbroken, so nothing degraded or down is ever behind this count.\n"},"operational":{"type":"number","description":"How many components the vendor publishes that are operational,\ncounting the omitted ones - a tally of the whole roster, not of\nwhat is listed.\n"},"maintenance":{"type":"number","description":"How many components the vendor publishes that are under\nscheduled maintenance, counting the omitted ones - a tally of\nthe whole roster, not of what is listed. Maintenance is capped\nas filler alongside `operational`, not protected like a fault:\nsome of these components may be missing from `components` even\nthough every fault-status component is present.\n"},"notOperational":{"type":"number","description":"How many components the vendor publishes that are in a fault\nstatus (degraded, partial or major outage, or unknown) - this\nnever includes `maintenance`, which is filler, not a fault. All\nof these appear in `components`; the cap can never drop one.\n"},"summary":{"type":"string","description":"The same tally in one line, for a caller that logs or shows this\nrather than branching on it.\n"}}},"ClosedIncidentSummary":{"type":"object","required":["name","lastUpdatedAt","summary"],"description":"An incident the vendor still has posted but has not touched in over\n14 days, reported here instead of as `activeIncident`. Several\nvendors leave a long-finished regional event open on their status\nsurface; serving it as the active incident on a snapshot dated\nminutes ago misreads it as current.\n","properties":{"name":{"type":"string","description":"The incident's title, as the vendor states it."},"lastUpdatedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the vendor last updated the incident, from the vendor's own\n`updatedAt`. Null when the vendor publishes no update time for\nit at all, in which case the age in `summary` is measured from\nthe incident's start instead. An incident whose update time the\nvendor does publish, but in a form this API will not restate as\na machine instant, is never reported here: it stays in\n`activeIncident`, because an undatable update could be newer\nthan anything else on the incident.\n"},"summary":{"type":"string","description":"One sentence: how long the incident has sat untouched, measured\nagainst `lastCheckedAt`, and whether the vendor ever published\nupdate text for it.\n"}}},"MonitorDetail":{"type":"object","required":["slug","name","homepageUrl","overallStatus","components","activeIncident","lastCheckedAt","sourceUpdatedAt","staleness"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"homepageUrl":{"type":"string"},"overallStatus":{"$ref":"#/components/schemas/StatusLevel"},"components":{"type":"array","description":"The vendor's components, degraded/outage/unknown ones first\n(worst first), then filler - operational components and\ncomponents under scheduled maintenance, in the vendor's own\norder - until the list reaches 12 entries.\n\nEvery component in a fault status (degraded, partial outage,\nmajor outage, or unknown) is always listed, even when that\ntakes the list past 12 - a filler component is never kept in\nplace of one that is actually broken. `maintenance` is filler,\nnot a protected fault status: it names scheduled vendor work,\nnot a problem, and a vendor that puts dozens of individual sites\ninto `maintenance` for one scheduled window (Cloudflare's edge\nPoPs are the standing example) is not having dozens of\nincidents. Treating it as a protected fault would let that kind\nof maintenance window sell as long a body as a real outage,\nwhich defeats the point of the cap. What the cap drops is\ntherefore always either an operational component or one under\nmaintenance, and `componentsOmitted` states how much of each and\nwhat the full roster tallies to. Vendors that publish dozens or\nhundreds of components (Cloudflare, Twilio, Auth0) are the\nreason the cap exists: an agent paying for one decision should\nnot have to read 300 rows of \"operational\" to find the three\nthat are actually broken.\n\nEmpty when the vendor publishes no component breakdown at all.\n","items":{"$ref":"#/components/schemas/StatusComponent"}},"componentsOmitted":{"$ref":"#/components/schemas/ComponentsOmitted","nullable":true,"description":"Null when nothing was omitted - the `components` list above is\nthen the vendor's whole roster.\n"},"activeIncident":{"$ref":"#/components/schemas/Incident","nullable":true,"description":"The vendor's open incident, when there is one the vendor is\nstill working: null both when the vendor has nothing open and\nwhen what it has open has not been updated in over 14 days, in\nwhich case `closedIncidentSummary` carries it instead.\n"},"closedIncidentSummary":{"$ref":"#/components/schemas/ClosedIncidentSummary","nullable":true,"description":"Null unless an incident was withheld from `activeIncident` for\nbeing stale. Never populated at the same time as\n`activeIncident`.\n"},"lastCheckedAt":{"type":"string","format":"date-time","nullable":true,"description":"When this API last checked the vendor. Our clock, not theirs.\n"},"sourceUpdatedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the vendor itself last republished the status surface this\nsnapshot was read from, as the vendor's own feed states it\n(Statuspage and the feeds that copy its schema publish\n`page.updated_at`).\n\nNull whenever the source publishes no such field - every\nmonitor whose status page has to be read as HTML, and the JSON\nfeeds that omit it. It is never substituted with a timestamp of\nours: `lastCheckedAt` and `staleness` are our poll clock and\nanswer a different question. A vendor bumps this on every\nrepublish, including ones that changed nothing, so it is\nprovenance rather than a change signal - use\n`GET /api/monitors/{slug}/changes` for what actually moved.\n"},"checkIntervalMinutes":{"type":"number","description":"Minutes between checks on the cadence this vendor is on right now: the healthy interval while nothing is wrong, the faster retry interval while the vendor reports a problem or the last check failed (stretched while a vendor is blocking our checks). Not the time until the next check."},"staleness":{"$ref":"#/components/schemas/Staleness"}}},"ChangeRecord":{"type":"object","required":["id","changeType","previousStatus","newStatus","createdAt"],"properties":{"id":{"type":"number"},"changeType":{"type":"string","enum":["status_change","component_change","incident_opened","incident_updated","incident_resolved"]},"previousStatus":{"type":"string","nullable":true,"description":"The monitor's *overall* status before this change, stamped on\nevery row a single check wrote - not the subject of the row.\nWhat the row itself is about is in `detail`.\n"},"newStatus":{"type":"string","nullable":true,"description":"The monitor's *overall* status after this change. Equal to\n`previousStatus` on a row whose subject moved underneath a\nheadline that did not.\n"},"detail":{"nullable":true,"description":"What actually moved. The shape is discriminated by\n`changeType`, and is one of:\n\n- `status_change` - `{ previous, next }`, the overall status\n  before and after, both `StatusLevel` values.\n- `component_change` - `{ changed }`, only the components whose\n  status moved, sorted by name. Each entry is\n  `{ name, previous, next }`, where `previous` is null for a\n  component the vendor added and `next` is null for one it\n  dropped. Components that did not move are not listed:\n  `GET /api/monitors/{slug}` is the call that reports component\n  state, ordered and capped as its schema describes.\n- `incident_opened` / `incident_resolved` - `{ incident }`, the\n  incident as it read at that moment.\n- `incident_updated` - `{ previous, next }`, the incident\n  before and after the vendor's update.\n\nNull only in the degenerate case of a stored row whose payload\ncould not be read back in any of these shapes.\n","oneOf":[{"$ref":"#/components/schemas/ChangeDetailStatusChange"},{"$ref":"#/components/schemas/ChangeDetailComponentChange"},{"$ref":"#/components/schemas/ChangeDetailIncident"},{"$ref":"#/components/schemas/ChangeDetailIncidentUpdated"}]},"createdAt":{"type":"string","format":"date-time","description":"When this API observed the change. Vendors backdate; this is\nour observation clock, not theirs.\n"}}},"ChangeDetailStatusChange":{"type":"object","description":"`detail` of a `status_change`: the overall status before and after.","required":["previous","next"],"properties":{"previous":{"$ref":"#/components/schemas/StatusLevel"},"next":{"$ref":"#/components/schemas/StatusLevel"}}},"ComponentStatusDelta":{"type":"object","description":"One component whose status moved, and how.","required":["name","previous","next"],"properties":{"name":{"type":"string","description":"The component's display name as of the newer snapshot, or the\nolder one for a component that disappeared.\n"},"previous":{"allOf":[{"$ref":"#/components/schemas/StatusLevel"}],"nullable":true,"description":"Null when the vendor added this component."},"next":{"allOf":[{"$ref":"#/components/schemas/StatusLevel"}],"nullable":true,"description":"Null when the vendor dropped this component."}}},"ChangeDetailComponentChange":{"type":"object","description":"`detail` of a `component_change`: only the components that moved,\nsorted by name. Never the two full rosters.\n","required":["changed"],"properties":{"changed":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/ComponentStatusDelta"}}}},"ChangeIncident":{"type":"object","description":"An incident exactly as it read at the moment a change was recorded.\n\nSame fields as `Incident`, but the two timestamps are the vendor's\nown text rather than a normalized instant: this is a historical\nrecord of what the source stated, not a re-derived value. Most\nvendors publish ISO 8601 there and this is an ISO 8601 string;\nsome publish only a human-readable time (\"Mar 01 10:46 PM PST\"),\nand that is passed through as stated rather than reinterpreted\ninto an instant we cannot actually confirm. Parse defensively.\n`GET /api/monitors/{slug}` normalizes the *live* incident.\n","required":["name","status"],"properties":{"name":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"startedAt":{"type":"string","nullable":true},"updatedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"latestUpdate":{"type":"string","nullable":true,"description":"What the vendor's most recent update said at the moment this\nchange was recorded. Same field as `Incident.latestUpdate`.\n\nAbsent - not null - on rows recorded before this API published\nincident update text at all, and on any row whose source\ncarried none. A change row is a historical record and is never\nrewritten, so an older row simply does not carry the key.\n"}}},"ChangeDetailIncident":{"type":"object","description":"`detail` of an `incident_opened` or `incident_resolved`: the\nincident as it read at that moment.\n","required":["incident"],"properties":{"incident":{"$ref":"#/components/schemas/ChangeIncident"}}},"ChangeDetailIncidentUpdated":{"type":"object","description":"`detail` of an `incident_updated`: the incident before and after.","required":["previous","next"],"properties":{"previous":{"$ref":"#/components/schemas/ChangeIncident"},"next":{"$ref":"#/components/schemas/ChangeIncident"}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}}}}}