{
  "openapi": "3.1.0",
  "info": {
    "title": "CityFunIndex API",
    "summary": "The CityFunIndex Fun Score and all 23 underlying factor scores for every US city in the index — the licensed, key-gated dataset API.",
    "description": "The CityFunIndex API is a licensed, key-gated dataset API. The 0–100 Fun Score and factor scores are free to read on the website, but every programmatic and bulk request — including the raw measured value behind each factor — requires a paid API key and is metered one call per city record. Commercial and white-label use is licensed by deployment scope — see https://cityfunindex.com/pricing.\n\nThe dataset is recomputed daily and republished whenever its inputs change. Every record carries an `algorithm_version` you should pin against; within a major version no field is removed and no field type changes.",
    "version": "v2.1.0-dev",
    "termsOfService": "https://cityfunindex.com/terms",
    "contact": {
      "name": "CityFunIndex API",
      "url": "https://cityfunindex.com/api",
      "email": "api@cityfunindex.com"
    },
    "license": {
      "name": "CityFunIndex Custom License",
      "url": "https://cityfunindex.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://cityfunindex.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Licensed",
      "description": "The keyed, metered dataset API (/v1) — the full record INCLUDING the raw measured value behind each factor, withheld from the public CDN. Requires an API key and meters one call per city record (one call = one city); the bulk endpoints that return every city at once require the Business or Enterprise plan. See https://cityfunindex.com/pricing."
    }
  ],
  "paths": {
    "/v1/city/{slug}": {
      "get": {
        "operationId": "getLicensedCity",
        "summary": "One city in full, with raw measured values (licensed)",
        "description": "A city's full record INCLUDING the raw measured value behind each factor (`raw`) — the licensed dataset, served only through this keyed endpoint (there is no keyless public feed). Requires an API key. Costs ONE call (one city record) on a 2xx; a 404 / 503 / over-limit response is never charged. Available to every keyed plan — the targeted-lookup primitive lower tiers use instead of the bulk endpoints.",
        "tags": [
          "Licensed"
        ],
        "security": [
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The stable URL identifier for the city — taken from any CitySummary.slug in the public or keyed city index.",
            "schema": {
              "$ref": "#/components/schemas/CitySlug"
            },
            "example": "austin-tx"
          }
        ],
        "responses": {
          "200": {
            "description": "The city's full licensed record. Charged one call.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Burst-Limit": {
                "$ref": "#/components/headers/XRateLimitBurstLimit"
              },
              "X-RateLimit-Burst-Remaining": {
                "$ref": "#/components/headers/XRateLimitBurstRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LicensedCityDetail"
                },
                "examples": {
                  "austin": {
                    "summary": "Austin, TX — two of 23 pillars shown, each WITH its raw measured value",
                    "value": {
                      "slug": "austin-tx",
                      "name": "Austin",
                      "state": "TX",
                      "lat": 30.27,
                      "lng": -97.74,
                      "universal_score": 67,
                      "category": "Fun",
                      "universal_positive": 59.415851365301876,
                      "universal_negative": 51.535921986714115,
                      "raw_composite": 38.80148257061623,
                      "pillars": {
                        "eats": {
                          "percentile": 75.93984962406014,
                          "refreshed_at": "2026-06-04T22:43:13Z",
                          "score": 75.93984962406014,
                          "raw": 2143,
                          "polarity": "positive"
                        },
                        "crime": {
                          "percentile": 65.41353383458647,
                          "refreshed_at": "2026-06-04T22:43:13Z",
                          "score": 65.41353383458647,
                          "raw": 4123.5,
                          "polarity": "negative"
                        }
                      },
                      "similar_cities": [
                        "columbus-oh",
                        "charlotte-nc",
                        "raleigh-nc",
                        "san-diego-ca",
                        "kansas-city-mo"
                      ],
                      "pro_sports_teams": [
                        {
                          "league": "MLS",
                          "team": "Austin FC"
                        }
                      ],
                      "algorithm_version": "v2.1.0-dev",
                      "computed_at": "2026-06-04T22:43:13Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No record exists for that slug — the city has not entered the index, or the slug is misspelt. Not charged."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/DatasetUnavailable"
          }
        }
      }
    },
    "/v1/cities": {
      "get": {
        "operationId": "getLicensedCities",
        "summary": "The full city index in one response (bulk, Business+)",
        "description": "Every city's summary row in a single response — the bulk index. A BULK endpoint: restricted to the Business and Enterprise plans (403 `bulk_not_allowed` for lower tiers) and charged ONE call per city returned, so a full pull costs the whole dataset's worth of calls. Lower tiers fetch /v1/city/{slug} one at a time and discover the city list from the free public CDN. Charged only on a 2xx.",
        "tags": [
          "Licensed"
        ],
        "security": [
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The full city index. Charged one call per city returned.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Burst-Limit": {
                "$ref": "#/components/headers/XRateLimitBurstLimit"
              },
              "X-RateLimit-Burst-Remaining": {
                "$ref": "#/components/headers/XRateLimitBurstRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CitySummary"
                  }
                },
                "examples": {
                  "twoCities": {
                    "summary": "Two cities from the index (the real response carries every city — and costs that many calls)",
                    "value": [
                      {
                        "slug": "austin-tx",
                        "name": "Austin",
                        "state": "TX",
                        "lat": 30.27,
                        "lng": -97.74,
                        "universal_score": 68,
                        "category": "Fun"
                      },
                      {
                        "slug": "nashville-tn",
                        "name": "Nashville",
                        "state": "TN",
                        "lat": 36.16,
                        "lng": -86.78,
                        "universal_score": 61,
                        "category": "Fun"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/BulkNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/DatasetUnavailable"
          }
        }
      }
    },
    "/v1/dataset.csv": {
      "get": {
        "operationId": "getLicensedDatasetCsv",
        "summary": "The whole dataset as one CSV (bulk, Business+)",
        "description": "The full licensed dataset — every city, every factor's raw measured value — as one flat CSV (Content-Disposition: attachment). A BULK endpoint: Business and Enterprise only (403 `bulk_not_allowed` otherwise) and charged ONE call per city row, so the full pull costs the same as fetching every city individually. Charged only on a 2xx.",
        "tags": [
          "Licensed"
        ],
        "security": [
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The full dataset CSV. Charged one call per city row.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename=\"cityfunindex-full.csv\""
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Burst-Limit": {
                "$ref": "#/components/headers/XRateLimitBurstLimit"
              },
              "X-RateLimit-Burst-Remaining": {
                "$ref": "#/components/headers/XRateLimitBurstRemaining"
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                },
                "examples": {
                  "fullCsv": {
                    "summary": "Header + one city row (the real file carries every city, with every factor's raw value)",
                    "value": "slug,name,state,universal_score,category,eats_raw,eats_score,crime_raw,crime_score\naustin-tx,Austin,TX,68,Fun,2143,75.94,4123.5,65.41\n"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/BulkNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/DatasetUnavailable"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Your plan, allowance and month-to-date usage",
        "description": "The caller's plan, monthly city-record allowance, per-minute burst limit and usage this period. Authenticated but NOT metered and NOT quota-gated — checking your usage never counts against you or is refused.",
        "tags": [
          "Licensed"
        ],
        "security": [
          {
            "ApiKeyBearer": []
          },
          {
            "ApiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's usage snapshot.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Burst-Limit": {
                "$ref": "#/components/headers/XRateLimitBurstLimit"
              },
              "X-RateLimit-Burst-Remaining": {
                "$ref": "#/components/headers/XRateLimitBurstRemaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                },
                "examples": {
                  "business": {
                    "summary": "A Business key part-way through the month",
                    "value": {
                      "plan": "business",
                      "period": "2026-06",
                      "used": 412,
                      "quota": 1500,
                      "remaining": 1088,
                      "rate_limit_per_min": 300,
                      "metering": "one call = one city record"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send your API key as `Authorization: Bearer <key>`. Keys are server-to-server credentials — never expose them in browser code."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternatively send your API key in the `X-API-Key` header."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "schema": {
          "type": "integer"
        },
        "description": "Your monthly city-record allowance (IETF RateLimit draft). Omitted for the unlimited Enterprise plan."
      },
      "RateLimitRemaining": {
        "schema": {
          "type": "integer"
        },
        "description": "City records left in the monthly allowance after this response."
      },
      "RateLimitReset": {
        "schema": {
          "type": "integer"
        },
        "description": "Seconds until the monthly allowance window resets (start of next month, UTC)."
      },
      "XRateLimitBurstLimit": {
        "schema": {
          "type": "integer"
        },
        "description": "Your per-minute burst ceiling. Omitted for the unlimited Enterprise plan."
      },
      "XRateLimitBurstRemaining": {
        "schema": {
          "type": "integer"
        },
        "description": "City records left in the current minute's burst window after this response."
      },
      "RetryAfter": {
        "schema": {
          "type": "integer"
        },
        "description": "Seconds to wait before retrying — present on 429 (until the limited window resets) and 503."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key. `error.code` is one of missing_key, invalid_key, inactive_key or expired_key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BulkNotAllowed": {
        "description": "A bulk endpoint (/v1/cities or /v1/dataset.csv) was called on a plan without bulk access. `error.code` is bulk_not_allowed. Fetch cities individually via /v1/city/{slug}, or upgrade to Business or Enterprise.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "A metering limit was hit. `error.code` is quota_exceeded when the monthly city-record allowance is spent, or rate_limited when the per-minute burst ceiling is exceeded. Both carry Retry-After + RateLimit-* headers; neither charges the request.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "DatasetUnavailable": {
        "description": "The dataset has not been published yet (or its blob is temporarily unreadable). `error.code` is dataset_unavailable. Not metered — retry after the seconds in Retry-After.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "CitySlug": {
        "type": "string",
        "description": "URL-safe city identifier: lowercase city name and state abbreviation, joined by a hyphen.",
        "pattern": "^[a-z0-9-]+-[a-z]{2}$",
        "examples": [
          "austin-tx",
          "san-francisco-ca",
          "new-york-ny"
        ]
      },
      "StateAbbr": {
        "type": "string",
        "description": "Two-letter US state abbreviation.",
        "pattern": "^[A-Z]{2}$",
        "examples": [
          "TX",
          "CA",
          "NY",
          "DC"
        ]
      },
      "FunScore": {
        "type": "integer",
        "description": "The calibrated 0–100 Fun Score.",
        "minimum": 0,
        "maximum": 100
      },
      "Category": {
        "type": "string",
        "description": "The Fun Score band — Exceptional (≥85), Very Fun (70–84), Fun (50–69), Some Fun (30–49), Quiet (<30).",
        "enum": [
          "Exceptional",
          "Very Fun",
          "Fun",
          "Some Fun",
          "Quiet"
        ]
      },
      "Polarity": {
        "type": "string",
        "description": "Whether a factor lifts the score (positive) or drags it down (negative).",
        "enum": [
          "positive",
          "negative"
        ]
      },
      "AlgorithmVersion": {
        "type": "string",
        "description": "Semver-shaped scoring-algorithm version. A `-sample` suffix marks illustrative preview data — never integrate against it in production. A `-dev` suffix marks a pre-1.0 development release where the algorithm shape may still change between recomputes.",
        "pattern": "^v\\d+\\.\\d+\\.\\d+(-[A-Za-z0-9.-]+)?$",
        "examples": [
          "v2.1.0-dev",
          "v2.0.7-dev",
          "v2.0.0-dev-sample"
        ]
      },
      "CitySummary": {
        "type": "object",
        "description": "A lightweight per-city summary — one row of the keyed city index (/v1/cities).",
        "required": [
          "slug",
          "name",
          "state",
          "lat",
          "lng",
          "universal_score",
          "category"
        ],
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/CitySlug"
          },
          "name": {
            "type": "string",
            "description": "City name."
          },
          "state": {
            "$ref": "#/components/schemas/StateAbbr"
          },
          "lat": {
            "type": "number",
            "description": "Latitude, decimal degrees."
          },
          "lng": {
            "type": "number",
            "description": "Longitude, decimal degrees."
          },
          "universal_score": {
            "$ref": "#/components/schemas/FunScore"
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          }
        }
      },
      "PillarScore": {
        "type": "object",
        "description": "One factor's score for one city. The website and the scored product publish `percentile` and `score`, but NOT the underlying raw measurement; the raw value is the licensed dataset, returned only by the keyed /v1 API (see https://cityfunindex.com/pricing). All 23 factors are live and carry a real measurement. Zero of the 23 factors are deferred. The §6.5 gap-fill placeholder (`score: 50`, carried by the `gapfill: true` flag) can still appear per-cell where a single city has no observation at all for a single factor (e.g. a Census place outside the fetcher's coverage map). Distinct from `thin: true`, which flags a real measurement of zero; a cell carries one flag, the other, or neither.",
        "required": [
          "percentile",
          "score"
        ],
        "properties": {
          "percentile": {
            "type": "number",
            "description": "Where the underlying measurement sits among all cities, 0–100.",
            "minimum": 0,
            "maximum": 100
          },
          "score": {
            "type": "number",
            "description": "The saturated 0–100 factor score that feeds the composite.",
            "minimum": 0,
            "maximum": 100
          },
          "refreshed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the source data behind this factor was last refreshed. Null when the source has no per-record refresh timestamp."
          },
          "thin": {
            "type": "boolean",
            "description": "Present and true only when the factor's score sits on a zero measurement (the source returned nothing) rather than a real low value; omitted otherwise. A derived signal surfaced from the withheld raw measurement. Disjoint from `gapfill`."
          },
          "gapfill": {
            "type": "boolean",
            "description": "Present and true only when this city had no observation at all for the factor and the score is the §6.5 median placeholder (lands near the 50th percentile) rather than a measured value; omitted otherwise. A derived signal that keeps a placeholder honestly distinguishable from a real median. Disjoint from `thin`."
          }
        }
      },
      "CityDetail": {
        "type": "object",
        "description": "A city's full record — every field in CitySummary plus the per-factor breakdown and any curated facts (e.g. major-league teams).",
        "required": [
          "slug",
          "name",
          "state",
          "lat",
          "lng",
          "universal_score",
          "category",
          "universal_positive",
          "universal_negative",
          "raw_composite",
          "pillars",
          "similar_cities",
          "pro_sports_teams",
          "algorithm_version",
          "computed_at"
        ],
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/CitySlug"
          },
          "name": {
            "type": "string"
          },
          "state": {
            "$ref": "#/components/schemas/StateAbbr"
          },
          "lat": {
            "type": "number",
            "description": "Latitude, decimal degrees."
          },
          "lng": {
            "type": "number",
            "description": "Longitude, decimal degrees."
          },
          "universal_score": {
            "$ref": "#/components/schemas/FunScore"
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "universal_positive": {
            "type": "number",
            "description": "Weighted positive composite (0–100), before the negative drag."
          },
          "universal_negative": {
            "type": "number",
            "description": "Weighted negative composite (0–100) — the drag term."
          },
          "raw_composite": {
            "type": "number",
            "description": "positive − 0.4 × negative, before the 0–100 calibration."
          },
          "pillars": {
            "type": "object",
            "description": "Per-factor scores keyed by factor key (see PillarRegistryEntry).",
            "additionalProperties": {
              "$ref": "#/components/schemas/PillarScore"
            }
          },
          "similar_cities": {
            "type": "array",
            "description": "Slugs of the nearest-neighbour cities by factor profile.",
            "items": {
              "$ref": "#/components/schemas/CitySlug"
            }
          },
          "pro_sports_teams": {
            "type": "array",
            "description": "Major-league franchises the city hosts (NFL, NBA, MLB, NHL, MLS) — a curated display fact, NOT a scoring input. Empty array for cities without any major-league team. Attribution rule: a team is listed under its home-stadium city if that city is in the index, else its brand city. Sorted by canonical league order (NFL → NBA → MLB → NHL → MLS), then alphabetically by team.",
            "items": {
              "$ref": "#/components/schemas/ProSportsTeam"
            }
          },
          "algorithm_version": {
            "$ref": "#/components/schemas/AlgorithmVersion"
          },
          "computed_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601-Z timestamp of the recompute that wrote this record."
          }
        }
      },
      "ProSportsTeam": {
        "type": "object",
        "description": "A single major-league franchise. The list is curated from public franchise data, not measured; updates between recomputes appear when the underlying CSV is refreshed.",
        "required": [
          "league",
          "team"
        ],
        "properties": {
          "league": {
            "type": "string",
            "description": "Major-league code — one of NFL, NBA, MLB, NHL, MLS.",
            "enum": [
              "NFL",
              "NBA",
              "MLB",
              "NHL",
              "MLS"
            ]
          },
          "team": {
            "type": "string",
            "description": "Franchise name, e.g. `Dallas Cowboys` or `Los Angeles Dodgers`.",
            "examples": [
              "Dallas Cowboys",
              "Boston Celtics",
              "Inter Miami CF"
            ]
          }
        }
      },
      "PillarRegistryEntry": {
        "type": "object",
        "description": "A single factor's place in the registry — the key, its polarity, and its default composite weight.",
        "required": [
          "key",
          "polarity",
          "weight"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable factor key, e.g. `eats`, `live_music`, `crime`. Pillars in `CityDetail.pillars` use these as map keys."
          },
          "polarity": {
            "$ref": "#/components/schemas/Polarity"
          },
          "weight": {
            "type": "number",
            "description": "The default weight within its polarity group. Positive and negative weights each sum to 1.0.",
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "PillarsBundleCity": {
        "type": "object",
        "description": "One city's factor scores in the bundle, split by polarity.",
        "required": [
          "slug",
          "name",
          "state",
          "universal_score",
          "positive",
          "negative"
        ],
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/CitySlug"
          },
          "name": {
            "type": "string"
          },
          "state": {
            "$ref": "#/components/schemas/StateAbbr"
          },
          "universal_score": {
            "$ref": "#/components/schemas/FunScore"
          },
          "positive": {
            "type": "object",
            "description": "Positive-factor scores keyed by factor key (0–100). All sixteen positive factors are live and carry a real measurement; none are deferred. See the PillarScore schema for the §6.5 gap-fill placeholder that can still appear per-cell when a single city lacks data for a single factor.",
            "additionalProperties": {
              "type": "number"
            }
          },
          "negative": {
            "type": "object",
            "description": "Negative-factor scores keyed by factor key (0–100). All seven negative factors are live and carry a real measurement; none are deferred. See the PillarScore schema for the §6.5 gap-fill placeholder that can still appear per-cell when a single city lacks data for a single factor.",
            "additionalProperties": {
              "type": "number"
            }
          }
        }
      },
      "PillarsBundle": {
        "type": "object",
        "description": "The all-cities factor-score bundle the personalize tool is built on.",
        "required": [
          "algorithm_version",
          "computed_at",
          "pillars",
          "cities"
        ],
        "properties": {
          "algorithm_version": {
            "$ref": "#/components/schemas/AlgorithmVersion"
          },
          "computed_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601-Z timestamp of the recompute that produced this bundle."
          },
          "pillars": {
            "type": "array",
            "description": "The 23-entry default weight vector.",
            "items": {
              "$ref": "#/components/schemas/PillarRegistryEntry"
            }
          },
          "cities": {
            "type": "array",
            "description": "Every city's factor scores, split into positive and negative groups.",
            "items": {
              "$ref": "#/components/schemas/PillarsBundleCity"
            }
          }
        }
      },
      "LicensedPillarScore": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PillarScore"
          },
          {
            "type": "object",
            "description": "A licensed factor score — every public PillarScore field PLUS the raw measured value the public CDN withholds.",
            "properties": {
              "raw": {
                "type": "number",
                "description": "The raw measured value behind the factor — venue count, crime rate, per-capita figure, etc. This is the licensed dataset, reserved for keyed plans and stripped from the public CDN. For COMPOSITE factors (e.g. city_vibe) this is a blended percentile rather than a single measurement."
              },
              "polarity": {
                "$ref": "#/components/schemas/Polarity"
              }
            }
          }
        ]
      },
      "LicensedCityDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CityDetail"
          },
          {
            "type": "object",
            "description": "The keyed /v1/city/{slug} record: identical to the public CityDetail, except every pillar additionally carries its raw measured value (see LicensedPillarScore). The private bundle's contributing_venues (Google Places names) is never resold and is stripped from this response.",
            "properties": {
              "pillars": {
                "type": "object",
                "description": "Per-factor scores keyed by factor key, each WITH its raw measured value.",
                "additionalProperties": {
                  "$ref": "#/components/schemas/LicensedPillarScore"
                }
              }
            }
          }
        ]
      },
      "Usage": {
        "type": "object",
        "description": "The caller's plan, limits and month-to-date usage — the /v1/usage body. Never metered.",
        "required": [
          "plan",
          "period",
          "used",
          "quota",
          "remaining",
          "rate_limit_per_min",
          "metering"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "description": "The key's plan, e.g. startup, business, enterprise."
          },
          "period": {
            "type": "string",
            "description": "The current monthly allowance bucket, YYYY-MM (UTC).",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "used": {
            "type": "integer",
            "description": "City records spent in the current monthly period.",
            "minimum": 0
          },
          "quota": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The monthly city-record allowance; null means unlimited (Enterprise)."
          },
          "remaining": {
            "type": [
              "integer",
              "null"
            ],
            "description": "City records left this month; null means unlimited."
          },
          "rate_limit_per_min": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The per-minute burst ceiling; null means unlimited."
          },
          "metering": {
            "type": "string",
            "description": "A constant reminder of the metering unit.",
            "const": "one call = one city record"
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "The machine-readable error envelope every /v1 failure shares.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "status"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable error code.",
                "examples": [
                  "quota_exceeded",
                  "rate_limited",
                  "bulk_not_allowed",
                  "missing_key",
                  "dataset_unavailable"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation, safe to surface."
              },
              "status": {
                "type": "integer",
                "description": "The HTTP status code, repeated in the body for clients that only parse JSON."
              },
              "plan": {
                "type": "string",
                "description": "The caller's plan — present on metering/entitlement errors."
              }
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Full /api developer documentation",
    "url": "https://cityfunindex.com/api"
  }
}
