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 fetch-fun-score.js
// One city's full record, in any modern JS runtime. Keep the key
// server-side — it is a credential, never expose it in browser code.
const res = await fetch(
'https://cityfunindex.com/v1/city/nashville-tn',
{ headers: { Authorization: `Bearer ${process.env.CFI_KEY}` } },
);
if (!res.ok) {
throw new Error(`fun score fetch failed: ${res.status}`);
}
const city = await res.json();
console.log(`${city.name}: ${city.universal_score} (${city.category})`); fun_score.py
# Pull one city's full record. Requires `pip install requests`.
import os, requests
res = requests.get(
"https://cityfunindex.com/v1/city/nashville-tn",
headers={"Authorization": f"Bearer {os.environ['CFI_KEY']}"},
timeout=10,
)
res.raise_for_status()
city = res.json()
print(f"{city['name']}: {city['universal_score']} ({city['category']})") top_ten.mjs
// Bulk index — Business and Enterprise plans only. Node 18+ ships fetch.
const res = await fetch('https://cityfunindex.com/v1/cities', {
headers: { Authorization: `Bearer ${process.env.CFI_KEY}` },
});
const cities = await res.json();
// Top ten by Fun Score.
cities
.sort((a, b) => b.universal_score - a.universal_score)
.slice(0, 10)
.forEach((c, i) => console.log(
`${(i + 1).toString().padStart(2)} ${c.name}, ${c.state} ${c.universal_score}`,
)); 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.
| Field | Type | Description |
|---|---|---|
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.
| Field | Type | Description |
|---|---|---|
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:
| Field | Type | Description |
|---|---|---|
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.
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
-samplesuffix (e.g.v2.0.3-dev-sample) means the bundle is illustrative preview data, not the live index. Never integrate against a-sampleversion in production. A-devsuffix 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 | — | — | — | |
| 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.