{
  "pages": [
    {
      "path": "/",
      "purpose": "Home: entry, search, catalog overview.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/datasets",
      "purpose": "Search and browse the catalog with facets.",
      "params": [
        "q",
        "domain",
        "agency",
        "cadence",
        "freshness_state",
        "mirror_mode",
        "offset"
      ],
      "access": "anonymous"
    },
    {
      "path": "/datasets/:fourFour",
      "purpose": "One dataset: metadata, both freshness axes, columns, partners, exports.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/domains/:domain_key",
      "purpose": "One subject area's datasets, with the same browse facets.",
      "params": [
        "q",
        "agency",
        "cadence",
        "freshness_state",
        "mirror_mode",
        "offset"
      ],
      "access": "anonymous"
    },
    {
      "path": "/freshness",
      "purpose": "The overdue list against each publisher's own declared cadence.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/scorecard",
      "purpose": "Citywide KPIs by the city's own definitions, plus mirror lag, plus how much of the field corpus has been analysed.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/scorecard/fields",
      "purpose": "Datasets ranked by remaining analysis work, with the dataset-to-dimension web rendered.",
      "params": [
        "band",
        "dimension",
        "agency",
        "order",
        "offset"
      ],
      "access": "anonymous"
    },
    {
      "path": "/scorecard/fields/:fourFour",
      "purpose": "Every field of one dataset with its verdict, its method, and whether anybody read it.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/analyses",
      "purpose": "Published cross-dataset analyses, newest first.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/ontology",
      "purpose": "Concept scheme and column-by-column bindings for a scope.",
      "params": [
        "four_four*",
        "concept",
        "dimension"
      ],
      "access": "anonymous"
    },
    {
      "path": "/graph",
      "purpose": "Full-bleed graph workspace over the catalog graph.",
      "params": [
        "node",
        "scenario"
      ],
      "access": "anonymous"
    },
    {
      "path": "/drill",
      "purpose": "The records behind one aggregate cell (permalink).",
      "params": [
        "dataset",
        "metric",
        "label",
        "window",
        "coord*",
        "filter*"
      ],
      "access": "anonymous"
    },
    {
      "path": "/compose",
      "purpose": "N-way relate permalink: sequential pairwise relates, rendered server-side.",
      "params": [
        "dim",
        "scenario"
      ],
      "access": "anonymous"
    },
    {
      "path": "/relate",
      "purpose": "Two-dataset relate permalink.",
      "params": [
        "dim",
        "a",
        "aBind",
        "aMeasure",
        "aCol",
        "aVal",
        "b",
        "bBind",
        "bMeasure",
        "bCol",
        "bVal"
      ],
      "access": "anonymous"
    },
    {
      "path": "/s/:shareId",
      "purpose": "A shared view, readable without an account.",
      "params": [],
      "access": "anonymous"
    },
    {
      "path": "/alerts",
      "purpose": "Watches over saved views (sign-in required).",
      "params": [],
      "access": "session"
    },
    {
      "path": "/signed-in",
      "purpose": "Sign-in landing tab.",
      "params": [],
      "access": "session"
    }
  ],
  "proxies": [
    {
      "path": "/api/health",
      "methods": [
        "GET"
      ],
      "purpose": "Portal liveness. Answers locally; does not touch serve, so it is green whenever Next is serving.",
      "access": "anonymous",
      "note": "Liveness only — it cannot report an upstream fault. Use /api/ready to learn whether this portal can answer."
    },
    {
      "path": "/api/ready",
      "methods": [
        "GET"
      ],
      "purpose": "Portal readiness: 200 when the data upstream answers, 503 when it does not. The one surface on this host that turns red during an upstream outage.",
      "access": "anonymous"
    },
    {
      "path": "/api/analyses",
      "methods": [
        "GET"
      ],
      "purpose": "Published cross-dataset analyses.",
      "access": "anonymous"
    },
    {
      "path": "/api/datasets/suggest",
      "methods": [
        "GET"
      ],
      "purpose": "Type-ahead: name matches first, metadata matches second, never merged.",
      "params": [
        "q!"
      ],
      "access": "anonymous"
    },
    {
      "path": "/api/datasets/:fourFour",
      "methods": [
        "GET"
      ],
      "purpose": "Canonical metadata, both freshness axes, columns, attribution, lineage.",
      "access": "anonymous"
    },
    {
      "path": "/api/datasets/:fourFour/partners",
      "methods": [
        "GET"
      ],
      "purpose": "Ranked, dimension-tag-confirmed relatives of one dataset.",
      "params": [
        "limit",
        "offset"
      ],
      "access": "anonymous"
    },
    {
      "path": "/api/datasets/:fourFour/bindings/:partner",
      "methods": [
        "GET"
      ],
      "purpose": "Confirmed shared dimension bindings between two datasets.",
      "access": "anonymous"
    },
    {
      "path": "/api/dimensions",
      "methods": [
        "GET"
      ],
      "purpose": "Every conformed-dimension family with its live tagged count.",
      "access": "anonymous"
    },
    {
      "path": "/api/dimensions/:key/datasets",
      "methods": [
        "GET"
      ],
      "purpose": "One family's tagged members, paginated, with assertion tiers.",
      "params": [
        "limit",
        "offset"
      ],
      "access": "anonymous"
    },
    {
      "path": "/api/ontology",
      "methods": [
        "GET"
      ],
      "purpose": "The shared ontology for a scope: concept scheme, bindings, and the coverage quadruple. Unscoped returns the vocabulary only.",
      "params": [
        "four_four*",
        "concept",
        "dimension"
      ],
      "access": "anonymous",
      "note": "An answer over 8,388,608 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/field-coverage",
      "methods": [
        "GET"
      ],
      "purpose": "How much of the field corpus is analysed: the quadruple, the per-dataset bands, and the graph web's reach. Four counts, never one ratio.",
      "access": "anonymous",
      "note": "An answer over 8,388,608 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/field-coverage/datasets",
      "methods": [
        "GET"
      ],
      "purpose": "Datasets ranked by remaining analysis work. An unknown band or order is refused, never answered with an empty list.",
      "params": [
        "band",
        "dimension",
        "agency",
        "order",
        "limit",
        "offset"
      ],
      "access": "anonymous",
      "note": "An answer over 8,388,608 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/field-coverage/datasets/:fourFour",
      "methods": [
        "GET"
      ],
      "purpose": "Every column of one dataset with its verdict, its method, and whether anybody read it. The only surface that names an unanalysed field.",
      "access": "anonymous",
      "note": "An answer over 8,388,608 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/field-coverage/web",
      "methods": [
        "GET"
      ],
      "purpose": "The dataset-to-dimension web as nodes and edges. Bipartite: sharing a dimension is not the same claim as joining.",
      "params": [
        "dimension",
        "limit"
      ],
      "access": "anonymous",
      "note": "An answer over 8,388,608 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/rows/:fourFour",
      "methods": [
        "GET"
      ],
      "purpose": "Up to 50 distinct sample values of one named column. Not a raw-rows dump.",
      "params": [
        "column!"
      ],
      "access": "anonymous"
    },
    {
      "path": "/api/export/:fourFour",
      "methods": [
        "GET"
      ],
      "purpose": "Redirect to the dataset's newest curated parquet object.",
      "access": "anonymous",
      "note": "Known defect: the presigned URL currently resolves only inside the cluster. Use the /csv sibling until the presign is public."
    },
    {
      "path": "/api/export/:fourFour/csv",
      "methods": [
        "GET"
      ],
      "purpose": "A CSV sample of up to 1,000 rows, honest about being a sample.",
      "access": "anonymous"
    },
    {
      "path": "/api/geometry/:fourFour",
      "methods": [
        "GET"
      ],
      "purpose": "One dataset's geometry as a GeoJSON FeatureCollection, capped.",
      "params": [
        "limit"
      ],
      "access": "anonymous"
    },
    {
      "path": "/api/query",
      "methods": [
        "POST"
      ],
      "purpose": "Read-only SQL over pg_ro.<schema>.<table>, executed in serve's sandboxed executor.",
      "body": "{\"sql\": \"select …\"}",
      "access": "anonymous",
      "note": "Caps: 50,000 rows returned; plans estimated above 5,000,000 rows are refused; 30 s execution budget. Request body up to 25,000 bytes. An answer over 134,217,728 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/relate",
      "methods": [
        "POST"
      ],
      "purpose": "Compile and run a catalog-confirmed relate spec into one join, coverage and statement answer.",
      "body": "{\"dim\", \"a\": {\"fourFour\", \"bindingColumn\", \"measure\"}, \"b\": {…}, \"succession\"}",
      "access": "anonymous",
      "note": "Request body up to 8,192 bytes. A refusal is a named answer, not an error page: the refusal field says exactly what is missing. On the agency family, \"succession\" is \"mapped\" (default, crosses recorded renames) or \"strict\". An answer over 134,217,728 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/graph/query",
      "methods": [
        "POST"
      ],
      "purpose": "A named query from the published registry, with typed arguments.",
      "body": "{\"id\", \"args\"}",
      "access": "anonymous"
    },
    {
      "path": "/api/graph/expand",
      "methods": [
        "POST"
      ],
      "purpose": "One node's expansion through the configured explore graph.",
      "body": "{\"node\", \"limit\"}",
      "access": "anonymous"
    },
    {
      "path": "/api/graph/related/:fourFour",
      "methods": [
        "GET"
      ],
      "purpose": "Datasets sharing column names with this one, aggregated per partner.",
      "access": "anonymous"
    },
    {
      "path": "/api/standards/evidence",
      "methods": [
        "GET"
      ],
      "purpose": "The published bytes one conformance claim rests on.",
      "params": [
        "standard!",
        "field!",
        "four_four"
      ],
      "access": "anonymous"
    },
    {
      "path": "/api/openapi.json",
      "methods": [
        "GET"
      ],
      "purpose": "The full endpoint contract: OpenAPI 3.1 with request and response schemas and the caps written into them.",
      "access": "anonymous",
      "note": "An answer over 8,388,608 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/mcp",
      "methods": [
        "POST"
      ],
      "purpose": "The citytap MCP server. JSON-RPC 2.0 over POST; single JSON responses; no SSE. POST only — GET answers 405 here.",
      "body": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\"}",
      "access": "anonymous",
      "note": "An answer over 134,217,728 bytes is refused as 500 relay-response-over-cap."
    },
    {
      "path": "/api/refresh/capacity",
      "methods": [
        "GET"
      ],
      "purpose": "How many public refresh slots are open this hour.",
      "access": "anonymous",
      "note": "Reports headroom and grants none; costs no portal request. Always 200: a null capacity carries capacity_error naming whether the upstream declined or could not be reached."
    },
    {
      "path": "/api/refresh/:fourFour",
      "methods": [
        "POST"
      ],
      "purpose": "Ask for a mirror refresh of one dataset.",
      "access": "anonymous",
      "note": "A bearer is optional. Without one the request is granted only while the public lane is above its reserve. The status carries the verdict: 202 with Location for a submission, 200 for already_fresh, 429 with Retry-After for a throttle, 409 for a refusal, 503 with refusal refresh-failed for a failure, 503 with refusal refresh-gatekeeper-unreachable and Retry-After while the gatekeeper is not answering. The verdict object is in the body whenever there is one."
    },
    {
      "path": "/api/refresh/status/:id",
      "methods": [
        "GET"
      ],
      "purpose": "Progress of one refresh request, addressed by its sealed id.",
      "access": "anonymous",
      "note": "The id is unguessable; reachable only from a submitted request."
    },
    {
      "path": "/api/alerts",
      "methods": [
        "GET",
        "POST"
      ],
      "purpose": "The caller's own watches.",
      "access": "bearer"
    },
    {
      "path": "/api/views/trail",
      "methods": [
        "POST"
      ],
      "purpose": "Store one inquiry-state document with its provenance trail.",
      "access": "bearer"
    },
    {
      "path": "/api/auth/start",
      "methods": [
        "GET"
      ],
      "purpose": "Begin the sign-in exchange.",
      "access": "session"
    },
    {
      "path": "/api/auth/callback",
      "methods": [
        "GET"
      ],
      "purpose": "Complete the sign-in exchange.",
      "access": "session"
    },
    {
      "path": "/api/auth/session",
      "methods": [
        "GET"
      ],
      "purpose": "Who the current session is.",
      "access": "session"
    },
    {
      "path": "/api/auth/logout",
      "methods": [
        "POST"
      ],
      "purpose": "End the session.",
      "access": "session"
    }
  ],
  "docs": {
    "llms": {
      "path": "/llms.txt",
      "contentType": "text/plain; charset=utf-8",
      "cacheSeconds": 3600,
      "purpose": "The agent front door: what this host serves and where to start."
    },
    "llmsFull": {
      "path": "/llms-full.txt",
      "contentType": "text/plain; charset=utf-8",
      "cacheSeconds": 3600,
      "purpose": "The full-text companion to /llms.txt: the index, the agent guide and the complete endpoint table in one document."
    },
    "agents": {
      "path": "/agents.md",
      "contentType": "text/markdown; charset=utf-8",
      "cacheSeconds": 3600,
      "purpose": "The agent guide: pages, quickstarts, the join algebra worked end-to-end."
    },
    "surfaces": {
      "path": "/surfaces.json",
      "contentType": "application/json",
      "cacheSeconds": 300,
      "purpose": "This table, serialized: every page and endpoint with params and pointers."
    },
    "robots": {
      "path": "/robots.txt",
      "contentType": "text/plain; charset=utf-8",
      "cacheSeconds": 3600,
      "purpose": "Crawl policy and content signals."
    }
  },
  "feeds": {
    "pod": {
      "path": "/data.json",
      "contentType": "application/json",
      "standard": "Project Open Data 1.1"
    },
    "dcat": {
      "path": "/dcat.jsonld",
      "contentType": "application/ld+json",
      "standard": "DCAT-US 3"
    }
  },
  "mcp": {
    "path": "/api/mcp",
    "transport": "JSON-RPC 2.0 over POST; single JSON responses; no SSE; POST only.",
    "sessionHeader": "x-citytap-session",
    "tools": [
      "coverage_scorecard",
      "dataset_freshness",
      "dataset_rows",
      "graph_expand",
      "graph_related",
      "list_datasets",
      "list_dimensions",
      "ontology_bindings",
      "open_in_browser",
      "relate",
      "suggest_datasets"
    ]
  },
  "corpus": {
    "describes": "Every column in the catalog, whatever this bundle's scope. Present on every response so an agent can size the investigated surface without dumping it — it is NOT the selection above.",
    "columns_total": 56681,
    "bound": 4487,
    "refused": 10870,
    "insufficient": 5081,
    "unanalysed": 36243,
    "as_of": "2026-10-10T13:52:40.772Z"
  }
}