Developers

The CityFunIndex API

Every Fun Score, every ranking and all 23 factors — as clean, documented JSON. A licensed, key-gated API: paid plans, metered one call per city record.

Cities
134
States
50 + DC
Factors
23 16 up · 7 drag
Cadence
daily
Endpoints
4
API key
Required
Metering
1 call = 1 city

The endpoints

The data is a licensed product. Every programmatic and bulk pull runs through a keyed, versioned endpoint set at https://cityfunindex.com/v1 — the same scores the site runs on, plus the raw measured value behind every factor. The website itself stays free to browse; the API is the paid way to build on the data.

  • /v1/city/{slug} — one city in full, with the complete factor breakdown and raw values.
  • /v1/cities — the whole index in one response. Business+
  • /v1/dataset.csv — every city flattened to a single CSV. Business+
  • /v1/usage — your plan, allowance and usage this period. Never metered.

Quick start

Provision a key with any paid plan, then send it on every request — Authorization: Bearer <key> or X-API-Key. Each endpoint is a plain HTTPS GET that returns JSON. The same call in four common runtimes:

shell

# one city in full — every factor score AND its raw measured value
curl -H "Authorization: Bearer $CFI_KEY" \
  https://cityfunindex.com/v1/city/nashville-tn

Field-by-field reference for each endpoint follows below. Definitions for every term — Fun Score, raw composite, pillar, calibration — are in the glossary.

GET /v1/cities Business+

The whole city index in one response — one lightweight summary per city. Costs one call per city returned, so it is gated to the Business and Enterprise plans; lower tiers fetch cities one at a time via /v1/city/{slug}. Returns a JSON array of city objects.

FieldTypeDescription
slug string Stable URL identifier, e.g. "nashville-tn" — use it to build the per-city endpoint.
name string City name.
state string Two-letter state abbreviation.
lat number Latitude, decimal degrees.
lng number Longitude, decimal degrees.
universal_score number The Fun Score — calibrated 0–100.
category string Band label: Exceptional, Very Fun, Fun, Some Fun or Quiet.

Example response

[
  {
    "slug": "nashville-tn",
    "name": "Nashville",
    "state": "TN",
    "lat": 36.16,
    "lng": -86.78,
    "universal_score": 60,
    "category": "Fun"
  }
]

GET /v1/city/{slug}

One city in full — the targeted lookup every plan can call. Carries every field from the index above — slug, name, state, lat, lng, universal_score and category — plus the composite breakdown, all 23 scored factors and the nearest-neighbour cities. The fields below are the additions; the index fields are not repeated in the table. {slug} is the slug from the index — for example /v1/city/nashville-tn.

FieldTypeDescription
universal_positive number Weighted positive composite (0–100), before the negative drag.
universal_negative number Weighted negative composite (0–100) — the drag term.
raw_composite number positive − 0.4 × negative, before the 0–100 calibration.
pillars object The 23 scored factors, keyed by pillar key. Each value is a PillarScore (below).
similar_cities string[] Slugs of the nearest-neighbour cities by factor profile.
pro_sports_teams { league, team }[] Major-league franchises the city hosts (NFL, NBA, MLB, NHL, MLS). Empty array for cities without any team. Display-only, not a scoring input.
algorithm_version string Scoring algorithm version that produced the record.
computed_at string ISO-8601 timestamp of the recompute that wrote the record.

Each entry in pillars is a PillarScore:

FieldTypeDescription
percentile number 0–100 percentile rank of the underlying measurement across all cities.
score number Saturated 0–100 factor score that feeds the composite.
raw number | null The factor’s raw measured value in its native unit (restaurants per thousand, AQI, transit departures, …). The licensed dataset — returned only on the keyed /v1 endpoints, never on the website. null when the measurement is missing.
refreshed_at string | null When the source data behind this factor was last refreshed.
thin boolean? 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.
gapfill boolean? 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. Disjoint from thin (a cell is one, the other, or neither). Omitted otherwise.

Example response (one factor shown; on /v1 each PillarScore also carries a raw value)

{
  "slug": "nashville-tn",
  "name": "Nashville",
  "state": "TN",
  "universal_score": 60,
  "category": "Fun",
  "universal_positive": 56.28,
  "universal_negative": 55.10,
  "raw_composite": 34.24,
  "pillars": {
    "eats": {
      "percentile": 48.87,
      "score": 48.87,
      "refreshed_at": "2026-06-04T22:43:13Z"
    }
  },
  "similar_cities": ["indianapolis-in", "raleigh-nc", "charlotte-nc"],
  "pro_sports_teams": [{ "league": "MLS", "team": "Nashville SC" }],
  "algorithm_version": "v2.1.0-dev",
  "computed_at": "2026-06-04T22:43:13Z"
}

GET /v1/dataset.csv Business+

The whole licensed dataset as one flat CSV — every city, every factor’s score and its raw measured value, one row per city, ready for a spreadsheet, R or pandas. Served with Content-Disposition: attachment. Like /v1/cities, it is a bulk endpoint — Business and Enterprise only, charged one call per city row.

GET /v1/usage

Your plan, monthly city-record allowance, per-minute burst limit and the usage so far this period. Authenticated but never metered and never refused — checking your usage costs nothing and is available on every plan.

Example response

{
  "plan": "business",
  "period": "2026-06",
  "used": 412,
  "quota": 1500,
  "remaining": 1088,
  "rate_limit_per_min": 300,
  "metering": "one call = one city record"
}

OpenAPI spec

Every endpoint and every field above is described in a machine-readable OpenAPI 3.1 spec. Point any generator at it — openapi-generator, Speakeasy, Kiota, Orval — to scaffold a typed client SDK in any of 40+ languages in a single command.

/api/openapi.json

Versioning

Every published record carries an algorithm_version string. It is the contract you integrate against — when it changes, your client should treat the new bundle as a new dataset.

CityFunIndex is still in development — the live bundles carry a -dev suffix, which marks a pre-1.0 release where the algorithm shape may still change. The stability guarantees below are the policy we commit to at v1.0 (GA); until then, treat the schema as evolving and pin to the algorithm_version you tested against.

  • Format. vMAJOR.MINOR.PATCH (semver). A PATCH bump is a recompute on unchanged math; a MINOR bump tightens a calibration; a MAJOR bump changes the formula or factor set.
  • Stability window. Once CityFunIndex reaches v1.0 (GA), every MAJOR change will be announced at least 60 days before publication on Business and Enterprise plans, and the previous MAJOR will remain available at the same URL with a versioned suffix for one full quarter after release.
  • Suffixes. A -sample suffix (e.g. v2.0.3-dev-sample) means the bundle is illustrative preview data, not the live index. Never integrate against a -sample version in production. A -dev suffix marks the current pre-1.0 build, whose schema may still change.
  • Field stability. Once CityFunIndex reaches v1.0 (GA), no field will be removed and no field type will change within one MAJOR version. New fields may be added — your parser should ignore unknown keys at any stage.
  • Where to track changes. Every API, data and site change is logged on the public changelog, newest first.

Errors

Error handling is HTTP-status-shaped and intentionally narrow. The metering responses (401, 403, 429) have their own table under metering & rate limits; the status codes below cover the rest.

Status What it means What to do
200 Success — the response body is the JSON for that endpoint. Validate against the field tables above; keep `algorithm_version` to detect a breaking refresh.
301 / 308 A trailing-slash or capitalisation mismatch redirected your request. Fetch the canonical URL (lowercase slug, no trailing slash) so your client does not pay the redirect cost on every call.
404 No record exists for that slug. The slug may be misspelt, or the city has not yet entered the index. Compare against the city index (/v1/cities, or the slugs shown across the site) — every published slug is in the index. Treat 404 as "city not covered yet," not a transient error.
5xx A CDN-edge or upstream outage. Rare — the bundles are static. Retry with exponential backoff (1s, 2s, 4s). The data does not move between retries, so an aggressive cache layer in front of your client is the right long-term fix.

Plans & scope

Every plan is keyed and paid — there is no free feed. A plan unlocks the raw measured values behind every score, a versioned schema and a delivery SLA. Scores are recomputed daily; the underlying data refreshes on a tiered schedule, from weekly for events to quarterly or annual for slow government feeds. Each plan combines a monthly allowance of city records (one call = one city), a per-minute burst limit, and a licence scope; what scales with each rung is the call allowance, the burst ceiling, bulk access, the deployment scope and the SLA. Lower rungs fetch cities one at a time and cannot pull the whole index in a month — the bulk endpoints are Business and Enterprise only.

Plan Price Scope Records / mo Burst Bulk White-label Support SLA
Startup $49/mo Single site / app 50/mo 60/min Email
Business $249/mo Up to 5 domains 1,500/mo 300/min Yes Yes Priority 99.9% uptime · versioned schema
Enterprise From $1,000/mo Unlimited · offline Unlimited 1,200/min Yes Yes Dedicated CSM Custom uptime SLA · indemnification available

See full plan details for everything each tier includes, or how businesses license the dataset.

Authentication

Every /v1 request is authenticated with your API key, on a versioned, stable schema under the versioning policy. It is metered per city record — one call = one city — against a monthly allowance and a per-minute burst limit (see metering & rate limits). Send the key on every request — either header works:

# Authorization: Bearer <key>   (or)   X-API-Key: <key>
curl -H "Authorization: Bearer $CFI_KEY" \
  https://cityfunindex.com/v1/city/nashville-tn

Keys are issued with a Startup, Business or Enterprise plan. Provision and manage one — and view live usage — from your account dashboard, or email us for help. An unauthenticated request returns 401; the response says which header to send.

Metering & rate limits

The keyed API is metered by city record: one call = one city. /v1/city/{slug} costs one call; a bulk response from /v1/cities or /v1/dataset.csv costs one call per city it returns. Two independent limits apply — a monthly allowance (the economic lever, scaled by plan) and a per-minute burst ceiling (service protection). Both meter only on a successful (2xx) response; the free /v1/usage check is never metered.

Every keyed response carries its budget in the headers, so a client can self-throttle without guessing:

RateLimit-Limit: 1500          # your monthly city-record allowance
RateLimit-Remaining: 1487      # city records left this month
RateLimit-Reset: 1717200000    # unix time the monthly window rolls over
X-RateLimit-Burst-Limit: 300   # per-minute burst ceiling
X-RateLimit-Burst-Remaining: 297

When a limit is hit the API returns 429 with a Retry-After header and a JSON body whose error field names which limit. Handle the metering responses explicitly:

Status & code What it means What to do
401 missing_key No key on the request, or an unknown/revoked key. Send your key in the Authorization: Bearer or X-API-Key header.
429 quota_exceeded You have spent your monthly city-record allowance (the error.code distinguishes it from a burst limit). Wait for RateLimit-Reset, or upgrade the plan for a larger allowance.
403 bulk_not_allowed A lower tier called a bulk endpoint (/v1/cities or /v1/dataset.csv). Fetch cities one at a time via /v1/city/{slug}, or upgrade to Business or Enterprise.
429 rate_limited You exceeded your per-minute burst ceiling. Back off for the seconds in Retry-After, then resume; smooth your request rate.

The Fun Score badge

Prefer a drop-in visual? Every city page carries a ready-made embed badge — a small <iframe> snippet that always links back to the full breakdown. Open any city page and look for “Put this score on your site.”

Using the data

  • Attribution is required. Credit “CityFunIndex” and link back to the city page the data describes.
  • The scores on the website are free to read and cite. Every city, state and factor page shows the 0–100 scores in the page itself — free to reference with attribution. Pulling them programmatically, in bulk, or with the raw measured values is what the licensed API is for.
  • Be a good neighbour. Most underlying signals refresh on a monthly-to-annual schedule, so caching responses for days at a time is the right default — not an optimisation.

Build on the Fun Score

The licensed API gives you the raw measured values behind every score, quarter-on-quarter history and trend lines, a versioned schema with a delivery SLA, and bulk access — built for platforms that put the Fun Score in front of their own users.