Quickstart
The REST API is hosted and answering today: sign up, get a token, and call it. The agent interface is hosted too, on the same account.
1. Install the CLI
The only real client today is a Python command-line tool — not a Python SDK object, not JS/Ruby/Go packages.
pip install fastopendata-client # installs the `fastopendata` command
2. Authenticate
The API uses short-lived JWTs (OAuth2 password flow), not API keys. There's no dashboard to generate one from — you authenticate against your own running server with a username/password created via /signup.
./.fastopendata_token and re-authenticates when it expires.fastopendata login # prompts for username / password, writes a token to # ./.fastopendata_token — re-authenticates automatically when it expires
503 at the geocode stage even though the data is there — worth one retry, more specifically. Every example below uses a Georgia address so that it is fast, not because the others are absent.3. Make your first call
Pass a structured or free-form address. The server geocodes it against a self-hosted Nominatim instance, resolves the match to the 2020 Census tract containing it, and returns that tract's attributes along with its county's, state's and PUMA's.
No account yet? The demo page calls /demo/address_data, which needs no token and returns a fixed 23-column subset of the same response.
fastopendata address --street-number "1 W Court Sq" --city "Decatur" --state "GA" # → {"match": {"geoid": "13089022501", "latitude": 33.7751, ...}, # "payload": {"tract": {...}, "county": {...}, "state": {...}, "puma": {...}}} # Coordinates only, no tract data: fastopendata geocode --street-number "1 W Court Sq" --city "Decatur" --state "GA"
GET /address_data
The main endpoint: one address in, its tract's attributes out. The request runs three stages — geocode, tract lookup, tract-attribute read — and an error names the stage that failed (see error behavior).
Query parameters
| Name | Type | Description |
|---|---|---|
| street | string | Street address, house number included. |
| city | string | City name. |
| county | string | County name. |
| state | string | State name or abbreviation. |
| postalcode | string | ZIP code. |
| amenity | string | Named place, e.g. a building name. |
| variables | string | Comma-separated column names to project. Omitted, every column is returned. |
The match must resolve to street level or better (Nominatim rank_address 26+), because the coordinates of a city-level match fall in an arbitrary tract. A coarser match returns 422 rather than a plausible wrong answer. Requires a bearer token from /token.
GET /address_data/free_form is the same endpoint taking the whole address as one address string.
GET /demo/address_data
What the demo page calls. No token, a tighter rate limit, and a fixed 23-column projection grouped by geography instead of the whole table. Takes street (required), city, state and postalcode; null fields are omitted from the response. Everything else about it is identical to /address_data.
curl -G https://fastopendata.com/demo/address_data \ --data-urlencode "street=1 W Court Sq" \ --data-urlencode "city=Decatur" --data-urlencode "state=GA"
POST /structured_geocode
Geocode a single structured address. This is the real, working endpoint today — it proxies the request to a self-hosted Nominatim instance and returns Nominatim's own response shape.
Body parameters
| Name | Type | Description |
|---|---|---|
| street | string | Street address. |
| city | string | City name. |
| county | string | County name. |
| state | string | State name or abbreviation. |
| postalcode | string | ZIP code. |
| country | string | Country name. |
All fields are optional individually, but you need enough of them for Nominatim to find a match. Requires a bearer token from /token.
GET /health
Unauthenticated liveness check.
Returns {"status": "ok"}.
Response shape
The address endpoints answer with two blocks: match, which says what the address resolved to, and payload, which carries the attributes of the tract, its county, its state and its puma — 881 columns in total, unevenly populated, with no /fields endpoint to describe them yet. Which block a number came from matters: a PUMA estimate describes an area considerably larger than the tract.
{
"match": {
"display_name": "1, West Court Square, Decatur, DeKalb County, Georgia, 30030, United States",
"latitude": 33.7750619,
"longitude": -84.2971945,
"rank_address": 30,
"geoid": "13089022501",
"state_fips": "13",
"county_fips": "089",
"tract_code": "022501"
},
"payload": {
"tract": { "nri_risk_rating": "Very Low", … },
"county": { "school_count": 159, … },
"state": { … },
"puma": { "pct_poverty_5yr": 0.0933, … }
}
}
The pct_* columns are shares between 0 and 1, not percentages. /structured_geocode is different: it passes a single Nominatim search result straight through, with no envelope.
{
"place_id": 127365481,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0",
"osm_type": "way",
"osm_id": 98765432,
"boundingbox": [33.7749, 33.7752, -84.2973, -84.2970],
"lat": 33.7750619,
"lon": -84.2971945,
"display_name": "1, West Court Square, Decatur, DeKalb County, Georgia, 30030",
"place_rank": 30,
"category": "building",
"type": "yes",
"importance": 0.71
}
Error behavior
The address endpoints run three stages behind one request, and two of them can produce a 404 that means the opposite thing. So the body names the stage that failed: {"detail": {"stage": "geocode", "message": "…"}}.
| Status | Stage | Meaning |
|---|---|---|
| 400 | geocode | Not enough of an address to search for. |
| 401 | — | Missing, invalid, or expired bearer token. |
| 404 | geocode | No match for the address. Fix the address. |
| 404 | tract_lookup | The match is outside any 2020 Census tract we hold. |
| 404 | tract_data | The address is fine; we have no attribute row for its tract. Retrying will not help. |
| 422 | precision | The match is coarser than street level, or outside the United States. |
| 422 | variables | A name in variables matches no column. |
| 429 | — | Rate limit exceeded — 60 requests/minute per IP by default, less on the demo route. |
| 503 | tract_lookup / tract_data | A store this request needs is unreachable. Temporary, and about us. |
Planned, not built
These are real goals for the project, listed here instead of documented as if they exist:
- A Python SDK. The CLI is the only client; everything else is
curl. - A batch endpoint for looking up many addresses in one request.
- A
/fieldsendpoint describing an actual schema, once one exists. - Coverage documentation: which of the 881 columns are populated, for which tracts, from which vintage.
- OpenStreetMap points of interest near an address. The ETL code ingests OSM; nothing joins it to a lookup yet.
- An API-key system. Authentication today is a username/password and a 12-hour JWT.
Methodology
Geocoding today runs entirely through a self-hosted Nominatim instance (OpenStreetMap data, ODbL 1.0 license) — there is no Census Bureau Geocoder integration, and no Google or other proprietary geocoding service involved.
Tract boundaries come from TIGER/Line, loaded into PostGIS; the point-in-polygon lookup runs there. The attribute columns come from the ETL pipelines in this repository (Snakemake-based) over Census ACS/PUMS, FEMA's National Risk Index, NCES school data, USDA rural–urban continuum codes, CJARS, and others — flattened into one row per tract, which is what a lookup reads. SIPP, AHS, Wikidata and Foursquare Places are ingested by the pipeline but not yet exposed as columns. See packages/fastopendata/config.toml for the dataset list and sources.
Source attribution
Geocoding data is © OpenStreetMap contributors, licensed ODbL 1.0, served via Nominatim per its usage policy. Census Bureau data (ACS/PUMS, TIGER/Line) and the federal datasets joined alongside it are public domain.