{
  "openapi": "3.1.0",
  "info": {
    "title": "UptimeProject Public API",
    "version": "v1",
    "description": "Read-only API for cloud provider uptime rankings and historical\navailability data. Backed by an independent fleet of probes\nmeasuring real cloud services on a 1-minute cadence; aggregated\ninto per-minute and per-day time-series rollups and served as\ncacheable JSON.\n\nFor a human-readable interactive reference with in-browser\n\"Test Request\" against the live API, visit\n[uptimeproject.org/docs/api](https://uptimeproject.org/docs/api/)\n(or just `GET /v1/docs` for a redirect).\n\nAll `/v1/*` GET responses are stamped with\n`Cache-Control: public, max-age=300, s-maxage=300` (5 minutes)\nby default. The OpenAPI spec endpoint uses a longer 1-hour TTL\nbecause the spec only changes on releases.\n\nErrors use a single envelope: `{\"error\": \"<human-readable\nstring>\"}`. The error string is for humans; clients should branch\non the HTTP status code.\n",
    "contact": {
      "url": "https://uptimeproject.org"
    },
    "license": {
      "name": "CC BY 4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    }
  },
  "servers": [
    {
      "url": "https://api.uptimeproject.org",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "services",
      "description": "Per-service metadata, current status, and headline availability."
    },
    {
      "name": "uptime",
      "description": "Time-series availability data for charts and timelines."
    },
    {
      "name": "leaderboard",
      "description": "Ranked best-of lists by category and time window."
    },
    {
      "name": "meta",
      "description": "Machine-readable spec and the docs redirect."
    }
  ],
  "paths": {
    "/v1/services": {
      "get": {
        "tags": [
          "services"
        ],
        "operationId": "listServices",
        "summary": "List active services",
        "description": "Returns every service registered as `active = true` in the\nregistry. Each row carries the service's identity, headline\navailability over the last 30 and 90 days, and current\nconsensus status. The list is intended for the static site's\ndirectory page and for downstream consumers who want a\nfull snapshot of the catalog.\n\nAvailability values come from the per-day rollup (excluding\n`unknown` minutes from the denominator); a `null` value means\n\"no measurements in the window yet\".\n",
        "responses": {
          "200": {
            "description": "A non-empty array of services. Order is unspecified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceSummary"
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/services/{id}": {
      "get": {
        "tags": [
          "services"
        ],
        "operationId": "getService",
        "summary": "Fetch a single service by id",
        "description": "Returns the same fields as `/v1/services` for one service,\nplus the active checks that produce its measurements and each\nprobe's most-recent view of the service. Deactivated services\nand unknown ids both return 404 — the API does not surface\n\"this service used to exist\" as a distinct state.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ServiceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Service exists and is active.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDetail"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/services/{id}/uptime": {
      "get": {
        "tags": [
          "uptime"
        ],
        "operationId": "getServiceUptime",
        "summary": "Time-series availability for one service",
        "description": "Returns availability data over the requested period as an\nordered series of buckets plus a rolled-up summary. Bucket\nsize is chosen by the period:\n\n| period | bucket size | typical points |\n|--------|-------------|----------------|\n| `24h`  | 5 minutes   | 288            |\n| `7d`   | 1 hour      | 168            |\n| `30d`  | 1 day       | 30             |\n| `90d`  | 1 day       | 90             |\n| `1y`   | 1 day       | 365            |\n\nThe summary's `availability_pct` is computed across the whole\nperiod (not as an average of per-bucket percentages) so partial\nbuckets at the leading edge don't get equal weight to full ones.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ServiceId"
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Time window to return. Defaults to `30d`.\n",
            "schema": {
              "$ref": "#/components/schemas/UptimePeriod"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Service exists and the requested period is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UptimeSeries"
                }
              }
            }
          },
          "400": {
            "description": "The `period` query parameter is not one of the supported values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/APIError"
                },
                "examples": {
                  "invalid_period": {
                    "value": {
                      "error": "invalid period"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/leaderboard": {
      "get": {
        "tags": [
          "leaderboard"
        ],
        "operationId": "getLeaderboard",
        "summary": "Ranked best-of list of active services",
        "description": "Returns active services ranked by average availability over\nthe requested window. Ranks are 1-based, dense (no gaps), and\nordered by availability DESC with `name ASC` as the\ntie-breaker. Services with no measurements in the window\ncollapse to `availability_pct: 0` and rank at the bottom\nwith `current_status: unknown` — they remain in the list\nrather than disappearing so the static site can render an\nexplicit \"no data yet\" treatment.\n\nThe `category` filter accepts any of the registry-defined\ncategory strings; an unknown value returns 400 (we do not\nsilently return an empty list — that would mask typos).\n",
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Window over which availability is averaged. Subset of\nthe values accepted by `/v1/services/{id}/uptime`:\n`24h` and `1y` are deliberately excluded for the\nranking use-case (too noisy and too lagging,\nrespectively). Defaults to `30d`.\n",
            "schema": {
              "$ref": "#/components/schemas/LeaderboardPeriod"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Optional category filter. When omitted, all active\nservices rank together; when present, only services in\nthe matching category are ranked.\n",
            "schema": {
              "$ref": "#/components/schemas/Category"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked entries (possibly empty after a category filter).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeaderboardResponse"
                }
              }
            }
          },
          "400": {
            "description": "`period` is not in the leaderboard whitelist, or\n`category` is non-empty and not a known category.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/APIError"
                },
                "examples": {
                  "invalid_period": {
                    "value": {
                      "error": "invalid period"
                    }
                  },
                  "invalid_category": {
                    "value": {
                      "error": "invalid category"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/openapi.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "operationId": "getOpenAPISpec",
        "summary": "Machine-readable API specification",
        "description": "Returns this OpenAPI 3.1 document as JSON. Served with a\n1-hour TTL — the spec only changes on releases, so longer\ncaching is appropriate.\n\nUseful for clients that generate code from OpenAPI (e.g.\n`openapi-typescript`). For a human-readable interactive\nview, use `/v1/docs` or visit\n[uptimeproject.org/docs/api](https://uptimeproject.org/docs/api/)\ndirectly.\n",
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "description": "An OpenAPI 3.1 document. See the OpenAPI specification.",
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/v1/docs": {
      "get": {
        "tags": [
          "meta"
        ],
        "operationId": "getDocsRedirect",
        "summary": "Redirect to the human-readable API reference",
        "description": "Returns a 302 redirect to\n[uptimeproject.org/docs/api](https://uptimeproject.org/docs/api/),\nthe Scalar-rendered interactive reference for this API.\n\nExposed on the API host so a developer who lands on the\nraw base URL (or a CLI that follows redirects) has a one-\nhop path to the human-readable view. The destination page\nis hosted on the marketing site and inherits its chrome\n(header, footer, dark mode), and bundles a build-time copy\nof this same spec so it renders even if the API is briefly\nunreachable. \"Test Request\" in the docs page targets this\nlive API directly via CORS.\n",
        "responses": {
          "302": {
            "description": "Redirect to the marketing-site docs page.",
            "headers": {
              "Location": {
                "description": "Absolute URL of the reference page.",
                "schema": {
                  "type": "string",
                  "format": "uri",
                  "examples": [
                    "https://uptimeproject.org/docs/api/"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Short TTL (60s) so a future migration of the docs\npage doesn't get pinned in browser caches.\n",
                "schema": {
                  "type": "string",
                  "examples": [
                    "public, max-age=60, s-maxage=60"
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "ServiceId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Slug-style service identifier from the registry, e.g.\n`aws-s3`, `cloudflare-cdn`. Lower-case, dash-separated;\nstable across releases.\n",
        "schema": {
          "type": "string",
          "minLength": 1,
          "pattern": "^[a-z0-9][a-z0-9-]*$",
          "examples": [
            "aws-s3",
            "cloudflare-cdn"
          ]
        }
      }
    },
    "responses": {
      "NotFound": {
        "description": "The service id is not registered, or the service is\ndeactivated. The two cases collapse to the same response;\nthe API does not expose deactivated-vs-never-existed.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/APIError"
            },
            "examples": {
              "not_found": {
                "value": {
                  "error": "service not found"
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server-side error. Body is the standard error\nenvelope; the `error` string is intentionally generic and\nclients should not try to parse it for diagnostics.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/APIError"
            },
            "examples": {
              "generic": {
                "value": {
                  "error": "internal server error"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Status": {
        "type": "string",
        "description": "Probe-consensus status for a service or bucket.\n\n- `up`       — at least a majority of probes succeeded\n- `degraded` — some probes failing but not the majority\n- `down`     — every probe failed\n- `unknown`  — fewer than 3 probes reported (insufficient\n               data for a consensus call)\n",
        "enum": [
          "up",
          "down",
          "degraded",
          "unknown"
        ]
      },
      "UptimePeriod": {
        "type": "string",
        "description": "Period accepted by `/v1/services/{id}/uptime`. Bucket size\nis fixed per period (see endpoint description).\n",
        "enum": [
          "24h",
          "7d",
          "30d",
          "90d",
          "1y"
        ],
        "default": "30d"
      },
      "LeaderboardPeriod": {
        "type": "string",
        "description": "Period accepted by `/v1/leaderboard`. Subset of `UptimePeriod`:\n`24h` is excluded as too noisy for ranking (one bad hour would\nflip a service to last place); `1y` is excluded as too lagging\n(rewards last-year stability over current performance).\n",
        "enum": [
          "7d",
          "30d",
          "90d"
        ],
        "default": "30d"
      },
      "Category": {
        "type": "string",
        "description": "Service category. Mirrors the registry's category names\nverbatim — note `object-storage` (hyphenated), not\n`storage`.\n",
        "enum": [
          "iaas",
          "cdn",
          "dns",
          "object-storage",
          "dev-infra",
          "email",
          "payments",
          "ai"
        ]
      },
      "APIError": {
        "type": "object",
        "description": "Canonical error envelope used by every non-2xx response.\nThe `error` string is human-readable and may evolve between\ndeploys — clients should branch on the HTTP status.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message.",
            "examples": [
              "service not found",
              "invalid period",
              "invalid category",
              "internal server error"
            ]
          }
        }
      },
      "ServiceSummary": {
        "type": "object",
        "description": "One row of `/v1/services` (and the embedded base of\n`ServiceDetail`). Carries identity, headline availability,\ncurrent consensus status, and the active-checks count\n(lets the static site sum across services without N+1-fanning\nout to per-service detail).\n",
        "required": [
          "id",
          "name",
          "category",
          "show_on_leaderboard",
          "homepage_url",
          "current_status",
          "checks_count",
          "availability_24h",
          "availability_7d",
          "availability_30d",
          "availability_90d"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Slug-style identifier from the registry.",
            "examples": [
              "aws-s3"
            ]
          },
          "name": {
            "type": "string",
            "description": "Human-readable service name.",
            "examples": [
              "Amazon S3"
            ]
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "region": {
            "type": "string",
            "description": "Provider region for regional services. Omitted for\nglobal services (CDNs, DNS resolvers, payment APIs).\n",
            "examples": [
              "us-east-1"
            ]
          },
          "provider": {
            "type": "string",
            "description": "Human-readable brand or parent-company string used as the\nservice-detail subtitle on the static site (\"Amazon Web\nServices · IaaS & Hosting\"). Editorial — every `aws-*`\nservice maps to \"Amazon Web Services\" regardless of the\nproduct name; `linode` maps to \"Akamai / Linode\" reflecting\nthe parent-co; not derivable from `id`/`name`. Omitted on\nempty (every live registry row has a provider after\nmigration 000010, but the omit-on-empty contract guards\nagainst drift on a row newly added without one).\n",
            "examples": [
              "Amazon Web Services",
              "Cloudflare",
              "Akamai / Linode"
            ]
          },
          "show_on_leaderboard": {
            "type": "boolean",
            "description": "Registry-side public-visibility flag. `true` (the default\nfor every existing service) → the service appears on the\nhomepage leaderboard *and* the `/services/` index page.\n`false` → the service is still actively probed, scraped,\nand queryable via `/v1/services/{id}` / `/v1/uptime`, but\nhidden from public ranking surfaces. Used for two\nregistry patterns: (a) newly-onboarded services in a\nprobation window pending enough history for a credible\n30d-period number, and (b) per-product-surface splits\n(e.g. the per-region OVH instance checks `ovh-instance-*`)\nkept around for Tier-1↔Tier-2 vantage-point analysis but\ntoo granular to surface as their own leaderboard rows.\nAlways emitted (no omitempty): \"missing\" would mean \"API\ndoesn't know\" — a different signal from \"publicly\nhidden\" — and would force consumers to guess.\n",
            "examples": [
              true,
              false
            ]
          },
          "homepage_url": {
            "type": "string",
            "format": "uri",
            "description": "Public landing page for the service. Always present\n(the static site links the service name to this URL).\n",
            "examples": [
              "https://aws.amazon.com/s3/"
            ]
          },
          "status_page_url": {
            "type": "string",
            "format": "uri",
            "description": "Public status page if the provider publishes one. Surfaced\non the static site's /service/{id}/ detail page as the\n\"Provider status\" chip next to the homepage link. Lifted\nfrom `ServiceDetail` up to `ServiceSummary` so the list\nendpoint's payload carries everything the static-site\ngetStaticPaths needs without an N-fanned-out per-service\ndetail fetch (the cost is one short string per row, ~2KB\ntotal at 40 services × 50-char URL). Omitted on empty\nbecause not every service has a public status page —\nQuad9 is the v1 registry's lone case, and the contract\nshould survive future similar gaps without forcing a `\"\"`\nplaceholder the frontend has to special-case.\n",
            "examples": [
              "https://health.aws.amazon.com/"
            ]
          },
          "current_status": {
            "$ref": "#/components/schemas/Status"
          },
          "checks_count": {
            "type": "integer",
            "description": "Number of active checks measuring this service. Inactive\nchecks (the placeholders shipped with `active=false` for\nservices that need ops setup) are excluded so the visible\ntotal matches what the fleet is actually executing. `0`\nfor a newly-registered service whose checks haven't\nlanded yet.\n",
            "minimum": 0,
            "examples": [
              3,
              1
            ]
          },
          "availability_24h": {
            "type": [
              "number",
              "null"
            ],
            "description": "Availability percent over the rolling trailing 24 hours,\ncomputed directly from the per-minute consensus aggregate\n(excludes `unknown` minutes from the denominator). The\nper-day rollup can't approximate this window because its\nbucket boundary is anchored to UTC midnight — averaging\n\"today (partial) and yesterday\" would bias against\nservices that were unhealthy hours ago vs minutes ago.\n`null` when the window has no measurements yet.\n",
            "minimum": 0,
            "maximum": 100,
            "examples": [
              99.99,
              null
            ]
          },
          "availability_7d": {
            "type": [
              "number",
              "null"
            ],
            "description": "Availability percent over the trailing 7 days, derived\nfrom the per-day rollup (same source as `availability_30d`\n/ `availability_90d`). `null` when the window has no\nmeasurements yet.\n",
            "minimum": 0,
            "maximum": 100,
            "examples": [
              99.98,
              null
            ]
          },
          "availability_30d": {
            "type": [
              "number",
              "null"
            ],
            "description": "Availability percent over the trailing 30 days, derived\nfrom the per-day rollup (excludes `unknown` minutes from\nthe denominator). `null` when the window has no\nmeasurements yet.\n",
            "minimum": 0,
            "maximum": 100,
            "examples": [
              99.97,
              null
            ]
          },
          "availability_90d": {
            "type": [
              "number",
              "null"
            ],
            "description": "Same as `availability_30d`, over 90 days.",
            "minimum": 0,
            "maximum": 100,
            "examples": [
              99.91,
              null
            ]
          }
        }
      },
      "ServiceDetail": {
        "type": "object",
        "description": "Body of `GET /v1/services/{id}`. Embeds every field of\n`ServiceSummary` (including the `status_page_url` chip lifted\nup there in v0.1.11 so list-endpoint consumers don't need a\nper-service detail fetch) and adds the active-checks list and\neach probe's most-recent view of the service.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ServiceSummary"
          },
          {
            "type": "object",
            "required": [
              "checks",
              "probes"
            ],
            "properties": {
              "checks": {
                "type": "array",
                "description": "Active checks for this service. May be empty for\na newly-registered service whose checks haven't\nlanded yet — always renders as `[]`, never `null`.\n",
                "items": {
                  "$ref": "#/components/schemas/CheckSummary"
                }
              },
              "probes": {
                "type": "array",
                "description": "Per-vantage-point current view of the service,\nderived from the most-recent minute each probe\nreported on (within a 5-minute lookback). A probe\nthat hasn't reported in that window is absent from\nthe array — the static site renders a \"pending\"\nplaceholder for unmapped probe ids. May be empty\nfor services with no recent measurements — always\nrenders as `[]`, never `null`.\n",
                "items": {
                  "$ref": "#/components/schemas/ProbeStatus"
                }
              }
            }
          }
        ]
      },
      "CheckSummary": {
        "type": "object",
        "description": "One active check on a service. Exposed publicly so the\nmethodology page can show \"we monitor X by doing Y\".\n",
        "required": [
          "id",
          "check_type",
          "target",
          "interval_sec"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable check identifier.",
            "examples": [
              "aws-s3-https-virginia"
            ]
          },
          "check_type": {
            "type": "string",
            "description": "Probe type. Currently one of `https`, `tcp`, `tls`,\n`dns`, `storage`. Documented as a string (not enum)\nso adding a new probe type is a non-breaking change.\n",
            "examples": [
              "https"
            ]
          },
          "target": {
            "type": "string",
            "description": "What the probe hits — URL, IP+port, hostname, etc.\nFormat depends on `check_type`.\n\nFor most checks this is the real probe target. For\nchecks that hit infrastructure exposing a proprietary\nidentifier we don't want public — e.g. an S3/R2 bucket\nname that would otherwise be an attack surface — the\nAPI substitutes a `redacted` placeholder of the same\nshape (e.g. `https://redacted.s3.eu-west-1.amazonaws.com/probe.bin`)\nso the methodology page still conveys \"we run an S3\nobject GET from eu-west-1\" without leaking the bucket\nidentifier. The probe still hits the real target; this\nsubstitution is only on the public wire shape.\n",
            "examples": [
              "https://s3.us-east-1.amazonaws.com/",
              "https://redacted.s3.eu-west-1.amazonaws.com/probe.bin"
            ]
          },
          "interval_sec": {
            "type": "integer",
            "description": "Probe cadence in seconds.",
            "minimum": 1,
            "examples": [
              60
            ]
          }
        }
      },
      "ProbeStatus": {
        "type": "object",
        "description": "One vantage point's most-recent view of a service, returned\ninside `ServiceDetail.probes`. Status follows the\nconsensus vocabulary (`up` / `down` / `degraded`) but is\ncomputed PER PROBE for the most-recent minute that probe\nreported on, aggregated across the service's checks within\nthat minute: `up` if every check succeeded, `down` if zero\nsucceeded, `degraded` for the mixed case. There is no\n`unknown` here — probes that haven't reported in the\n5-minute lookback are absent from the array (the static\nsite renders \"pending\" for an unmapped probe id).\n",
        "required": [
          "probe_id",
          "status",
          "latency_ms"
        ],
        "properties": {
          "probe_id": {
            "type": "string",
            "description": "Stable probe identifier from the fleet registry, e.g.\n`hzr-fsn1`. The static site uses this as the lookup key\nwhen joining `probes` with its display metadata\n(location, provider, region).\n",
            "examples": [
              "hzr-fsn1",
              "ovh-rbx",
              "do-nyc3"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "up",
              "down",
              "degraded"
            ],
            "description": "Per-probe consensus across the service's checks within\nthe most-recent reported minute. `unknown` is not a\nvalid value here (those probes are absent from the\narray entirely — see schema description).\n",
            "examples": [
              "up"
            ]
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "p50 latency observed by this probe in the most-recent\nminute, conservatively MAX-aggregated across the\nservice's checks within the bucket (so a service whose\nHTTPS check is fast but TLS check is slow shows the\nslower number). `null` when every same-bucket\nmeasurement failed — distinguishable from \"the probe\ndidn't report at all\" (absent from the array) so the\nfrontend can render the two cases differently.\n",
            "minimum": 0,
            "examples": [
              42,
              null
            ]
          }
        }
      },
      "UptimePoint": {
        "type": "object",
        "description": "One bucket on the uptime time series. Bucket size depends\non the requested period (see `/v1/services/{id}/uptime`).\n",
        "required": [
          "bucket",
          "availability_pct",
          "status",
          "down_minutes",
          "degraded_minutes",
          "latency_p50_ms"
        ],
        "properties": {
          "bucket": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp at the START of the bucket, UTC, RFC 3339\n(e.g. `2026-05-08T09:00:00Z`).\n"
          },
          "availability_pct": {
            "type": "number",
            "description": "Percent of measured minutes in the bucket that were\nclassified `up`. `0` when the bucket has no measured\nminutes (in which case `status` is `unknown`).\n",
            "minimum": 0,
            "maximum": 100
          },
          "status": {
            "$ref": "#/components/schemas/Status"
          },
          "down_minutes": {
            "type": "integer",
            "description": "Count of minutes in the bucket with consensus `down`.",
            "minimum": 0
          },
          "degraded_minutes": {
            "type": "integer",
            "description": "Count of minutes in the bucket with consensus `degraded`.",
            "minimum": 0
          },
          "latency_p50_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Success-count-weighted p50 (median) response time, in\nmilliseconds, across every (check, probe) pair inside\nthe bucket. Sourced from `measurements_1m` for the\n24h/7d periods and from `measurements_1d` for the\n30d/90d/1y periods. `null` when the bucket had zero\nsuccessful measurements (timeouts only) — frontend\ncharts should skip those points on the latency line\nrather than plot them as 0ms.\n",
            "minimum": 0,
            "examples": [
              87,
              null
            ]
          }
        }
      },
      "UptimeSummary": {
        "type": "object",
        "description": "Rolled-up totals for the whole period. `availability_pct`\nis count-weighted across every measured minute (NOT an\naverage of per-bucket percentages) so partial buckets\ndon't get equal weight to full ones.\n",
        "required": [
          "availability_pct",
          "total_down_minutes",
          "total_degraded_minutes",
          "latency_p50_ms"
        ],
        "properties": {
          "availability_pct": {
            "type": "number",
            "description": "`100 * total_up / (total_up + total_down + total_degraded)`\nacross every measured minute in the period. `0` when\nno minute had measured data.\n",
            "minimum": 0,
            "maximum": 100
          },
          "total_down_minutes": {
            "type": "integer",
            "description": "Sum of `down_minutes` across every point.",
            "minimum": 0
          },
          "total_degraded_minutes": {
            "type": "integer",
            "description": "Sum of `degraded_minutes` across every point.",
            "minimum": 0
          },
          "latency_p50_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Success-count-weighted p50 response time, in\nmilliseconds, across every successful measurement in\nthe period. `null` when the period contained zero\nsuccessful measurements. Useful as the headline\n\"Average response time\" for a service detail page.\n",
            "minimum": 0,
            "examples": [
              142,
              null
            ]
          }
        }
      },
      "UptimeSeries": {
        "type": "object",
        "description": "Body of `GET /v1/services/{id}/uptime`.",
        "required": [
          "service_id",
          "period",
          "points",
          "summary"
        ],
        "properties": {
          "service_id": {
            "type": "string",
            "description": "Echoed back from the path parameter."
          },
          "period": {
            "$ref": "#/components/schemas/UptimePeriod"
          },
          "points": {
            "type": "array",
            "description": "Buckets in ascending bucket-time order. Always\nrenders as a JSON array (never `null`); empty `[]`\nis valid for a service with no measurements yet.\n",
            "items": {
              "$ref": "#/components/schemas/UptimePoint"
            }
          },
          "summary": {
            "$ref": "#/components/schemas/UptimeSummary"
          }
        }
      },
      "LeaderboardEntry": {
        "type": "object",
        "description": "One ranked row in `/v1/leaderboard`.",
        "required": [
          "rank",
          "service_id",
          "name",
          "category",
          "availability_pct",
          "current_status"
        ],
        "properties": {
          "rank": {
            "type": "integer",
            "description": "1-based, dense rank within this response. Tie-breaker\nis `name ASC`. Clients should NOT recompute rank;\nthe server's tie-breaker is part of the methodology.\n",
            "minimum": 1
          },
          "service_id": {
            "type": "string",
            "description": "Slug-style identifier; cross-references `/v1/services/{id}`."
          },
          "name": {
            "type": "string",
            "description": "Human-readable service name."
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "availability_pct": {
            "type": "number",
            "description": "Average availability percent over the requested period.\nServices with no measurements collapse to `0` (paired\nwith `current_status: unknown`).\n",
            "minimum": 0,
            "maximum": 100
          },
          "current_status": {
            "$ref": "#/components/schemas/Status"
          },
          "rank_change": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Rank movement vs. the previous publication\n(positive = improved, negative = worsened). Always\nabsent in v1 — historical leaderboard state is not\nyet persisted. Field will appear automatically once\nthe backing storage exists.\n"
          }
        }
      },
      "LeaderboardResponse": {
        "type": "object",
        "description": "Body of `GET /v1/leaderboard`.",
        "required": [
          "period",
          "entries",
          "generated_at"
        ],
        "properties": {
          "period": {
            "$ref": "#/components/schemas/LeaderboardPeriod"
          },
          "category": {
            "$ref": "#/components/schemas/Category",
            "description": "Echoed when a `?category=` filter was applied;\nabsent on the unfiltered call.\n"
          },
          "entries": {
            "type": "array",
            "description": "Ranked rows, ordered by `rank ASC`. Always renders\nas a JSON array (never `null`); empty `[]` is valid\nafter a category filter that matches no services.\n",
            "items": {
              "$ref": "#/components/schemas/LeaderboardEntry"
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server wall-clock at response build time, UTC,\nRFC 3339. Useful for both cache debugging and\nrendering \"as of …\" on the static site.\n"
          }
        }
      }
    }
  }
}
