Skip to content

US Census Data

Plaza carries US Census Bureau boundaries and statistics as Plaza-curated datasets. Each boundary feature has its demographic and economic attributes on it, so “which tract is this point in, and what is its median income?” is one query. You query them like any other dataset: dataset("census-tracts") in PlazaQL, or datasets in the body of POST /api/v1/features.

The data is a U.S. Government work in the public domain. It is free to query, cache and redistribute, and it is compatible with the ODbL terms on the OpenStreetMap data you may combine it with. The attribution text is “U.S. Census Bureau”. See Attribution.

There are 36 datasets in four groups.

These are the precise legal and statistical boundaries. Use them for point-in-polygon lookups and for apportioning statistics to an area you draw.

SlugGeographyNotes
census-statesStates and equivalents56 features
census-countiesCounties and equivalents3,235 features
census-county-subdivisionsCounty subdivisionsTowns, townships, districts
census-tractsCensus tractsAbout 85,500 features
census-block-groupsBlock groupsAbout 242,000 features
census-placesIncorporated places and census designated places
census-zctasZIP Code Tabulation AreasStatistical areas, not postal routes
census-cbsasMetropolitan and micropolitan areas
census-congressional-districtsCongressional districts
census-state-legislative-upperState senate districts
census-state-legislative-lowerState house districts
census-school-districts-unifiedUnified school districts
census-school-districts-elementaryElementary school districtsNot every state has them
census-school-districts-secondarySecondary school districtsNot every state has them
census-urban-areasUrban areas

Eight siblings of the layers above end in -cb: census-states-cb, census-counties-cb, census-tracts-cb, census-block-groups-cb, census-places-cb, census-zctas-cb, census-cbsas-cb and census-congressional-districts-cb. They are generalized to 1:500,000, so they draw cleanly on a map and are much lighter than the precise files. They carry the same attributes. Use the precise datasets when a point close to a boundary matters.

census-gazetteer-places has every place as a point at its internal point, with land and water area. It’s a lightweight alternative to the place polygons when you only need a location.

Twelve datasets hold the complete American Community Survey (ACS) 5-year data profiles, tables DP02 to DP05, for tracts, counties and places. Each is a point at the geography’s internal point. The slug pattern is acs5-2024-dp03-tract: dp02 to dp05, then tract, county or place. Property keys are the lowercased Census variable codes, such as dp03_0062e, and the field dictionary has the label for each. Use these when you need a variable that isn’t in the curated set described below.

Each dataset records its vintage. Boundaries are the newest TIGER/Line release the Census Bureau has published. Attributes come from the ACS 2024 5-year release, the 2020 Census redistricting file (PL 94-171) and the newest Population Estimates.

After the first load, once auto-sync is turned on, every dataset checks for new data at 03:00 UTC on the first of each month. Until then a dataset is refreshed only when Plaza reloads it by hand. The check is cheap. Plaza reloads a dataset only when a boundary file or an attribute release has changed, then switches to the new copy in one step, so a query never sees a half-loaded dataset. Feature IDs stay stable across refreshes. Datasets explains vintage, refreshed_at and the sync history endpoint.

A dataset that hasn’t finished its first load is pending. GET /api/v1/datasets leaves pending datasets out unless you pass include_pending=true. Naming one in a query is an error until the load finishes:

dataset not yet available: census-tracts (status: pending)

The "plaza" and "all" keywords skip datasets that aren’t ready. Before you depend on a layer, check its status:

Terminal window
curl -H "x-api-key: $PLAZA_API_KEY" \
"https://plaza.fyi/api/v1/datasets?scope=plaza&include_pending=true"

Every feature’s ID is its GEOID, the Census Bureau’s code for the area. The geoid property holds the same value. GEOIDs nest, so a longer one starts with the shorter ones above it:

GeographyLengthExampleBuilt from
State236State FIPS code
County536061State + county
County subdivision10County + subdivision
Tract1136061010200County + tract
Block group12360610102001Tract + block group
Place73651000State + place
ZCTA510118The ZIP Code Tabulation Area itself
CBSA535620The core-based area code
Congressional district43612State + district

To get the county of a tract, take the first 5 characters of its GEOID. Always treat GEOIDs as strings. Leading zeros matter: Alabama’s state code is 01.

Boundary features carry geoid, name, aland and awater (land and water area in square meters), intpt_lat and intpt_lng (the internal point), vintage, and, for geographies that sit inside a state, state_fips and state_abbr. The rest are the statistics below.

Every level from tract up has about 250 curated ACS properties with plain names such as population, median_household_income, pct_below_poverty and median_gross_rent. Names are stable. If the Census Bureau renumbers a variable between releases, Plaza follows the label and your property name keeps working.

A few things to know:

  • Margins of error. Forty key estimates also have <name>_moe, the 90% margin of error. median_household_income_moe goes with median_household_income.
  • Missing values. The Census Bureau’s “no estimate” codes become null. Use is_number() in a filter if a null would break your expression.
  • 2020 counts. Properties ending in _2020 are counts from the 2020 Census, such as population_2020 and housing_units_2020. They are on states, counties, county subdivisions, tracts, block groups and places.
  • Population estimates. States, counties, places and county subdivisions also have popest_population, popest_births and related properties.
  • Block groups. The ACS doesn’t publish profile tables below the tract, so block groups carry detailed-table variables. They are named population_detailed and median_household_income_detailed. Tracts have both forms.

Census Variables lists the properties by theme: population and age, race and ethnicity, households, income and poverty, employment and commuting, education, housing, internet access, health insurance, and nativity and language. For one dataset, GET /api/v1/datasets/census-tracts/dictionary returns the same list with types and descriptions.

The most common question is “which census geography is this point in?” In PlazaQL, ask the layer you want:

// The tract at the Empire State Building, with its demographics
search(dataset("census-tracts"))
.contains(point(40.7484, -73.9856))
.first();

Over REST, put the dataset in datasets and the point in contains. Without type, only dataset features come back:

Terminal window
curl -X POST https://plaza.fyi/api/v1/features \
-H "x-api-key: $PLAZA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasets": ["census-tracts"],
"contains": { "type": "Point", "coordinates": [-73.9856, 40.7484] },
"limit": 1
}'

To get every level at once, reverse geocode with include=census. See Reverse geocoding.

Filter on any property with t["name"]. Census values are numbers, so wrap them in number() for comparisons:

// Tracts within a 15-minute walk with median household income above $75,000
$walk = isochrone(center: point(40.7484, -73.9856), time: 900, mode: "foot");
search(dataset("census-tracts"))
.intersects($walk)
.filter(number(t["median_household_income"]) > 75000);

The isochrone makes this a premium request. The same filter over a fixed bbox or polygon is one standard request.

In the REST body, tags matches property values exactly. Numeric comparisons such as “above 75,000” need PlazaQL.

Terminal window
curl -X POST https://plaza.fyi/api/v1/features \
-H "x-api-key: $PLAZA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasets": ["census-counties"],
"tags": { "state_abbr": "NY" },
"limit": 100
}'

Add up a statistic over any set of features. In PlazaQL, end the chain with .sum(), .min(), .max() or .avg():

// Total population of New York State's counties
search(dataset("census-counties"))
.filter(t["state_abbr"] == "NY")
.sum(number(t["population"]));

The REST twin is output plus expr, the name of the property to aggregate:

Terminal window
curl -X POST https://plaza.fyi/api/v1/features \
-H "x-api-key: $PLAZA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasets": ["census-counties"],
"tags": { "state_abbr": "NY" },
"output": "sum",
"expr": "population"
}'
{ "value": 19400000 }

The number is illustrative. Scalar aggregates come back as {"value": n} from both /features and /query. With output=count, the answer is {"counts": {...}, "total": n}.

group_by splits the aggregate by a property, in both interfaces:

// Population by state
search(dataset("census-counties"))
.group_by(t["state_abbr"])
.sum(number(t["population"]));
Terminal window
curl -X POST https://plaza.fyi/api/v1/features \
-H "x-api-key: $PLAZA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasets": ["census-counties"],
"output": "sum",
"expr": "population",
"group_by": "state_abbr"
}'
{ "groups": { "NY": 19400000, "NJ": 9500000, "CT": 3700000 }, "total": 3 }

The response is abridged and the numbers are illustrative. total is the number of groups.

Your area rarely lines up with tract lines. A tract that is 40% inside your polygon should count for 40% of its population. overlap_fraction is the share, from 0 to 1, of each feature’s area that falls inside a reference polygon. Multiply a statistic by it for an area-weighted total:

// People within a 15-minute walk, weighting each tract by how much of it is inside
$walk = isochrone(center: point(40.7484, -73.9856), time: 900, mode: "foot");
search(dataset("census-tracts"))
.intersects($walk)
.sum(number(t["population"]) * overlap_fraction($walk));

This assumes people are spread evenly across each tract. That’s a reasonable first estimate, and it gets better with smaller geographies. Use census-block-groups for a finer answer.

Over REST, set weight to overlap_fraction and pass the polygon as ref_polygon. Use the same polygon as the spatial filter. For an isochrone, take the geometry from the isochrone response:

Terminal window
curl -X POST https://plaza.fyi/api/v1/features \
-H "x-api-key: $PLAZA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasets": ["census-tracts"],
"intersects": {
"type": "Polygon",
"coordinates": [[[-74.02, 40.70], [-73.97, 40.70], [-73.97, 40.75], [-74.02, 40.75], [-74.02, 40.70]]]
},
"output": "sum",
"expr": "population",
"weight": "overlap_fraction",
"ref_polygon": {
"type": "Polygon",
"coordinates": [[[-74.02, 40.70], [-73.97, 40.70], [-73.97, 40.75], [-74.02, 40.75], [-74.02, 40.70]]]
}
}'

overlap_fraction also works in .filter() and .sort(). To see the fraction on each returned feature, use the .overlap_fraction() method in PlazaQL or enrich in REST. Each feature then has an overlap_fraction property:

search(dataset("census-tracts"))
.intersects($walk)
.overlap_fraction($walk);
Terminal window
curl -X POST https://plaza.fyi/api/v1/features \
-H "x-api-key: $PLAZA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasets": ["census-tracts"],
"intersects": { "type": "Polygon", "coordinates": [[[-74.02, 40.70], [-73.97, 40.70], [-73.97, 40.75], [-74.02, 40.75], [-74.02, 40.70]]] },
"enrich": "overlap_fraction",
"ref_polygon": { "type": "Polygon", "coordinates": [[[-74.02, 40.70], [-73.97, 40.70], [-73.97, 40.75], [-74.02, 40.75], [-74.02, 40.70]]] },
"limit": 50
}'

overlap_fraction is null for features with no area, such as the ACS point datasets. PlazaQL advanced covers the function in full.

A dataset and OpenStreetMap can share a query. This finds pharmacies in tracts where more than 20% of households have no vehicle:

$car_free = search(dataset("census-tracts"))
.filter(number(t["pct_no_vehicle"]) > 20);
search(node, amenity: "pharmacy").intersects($car_free);

The Playground has three census queries ready to run: a county lookup, affluent tracts in a walking radius, and population inside an area.

  • Census datasets are read-only.
  • Census statistics are survey estimates. Small areas have wide margins of error, so check _moe before you rank tracts by a close value.
  • Congressional districts carry ACS attributes only when the boundary file and the ACS release cover the same Congress. The TIGER boundaries can be one Congress ahead of the ACS. When they are, census-congressional-districts loads without ACS properties until the ACS catches up, while census-congressional-districts-cb follows the ACS.