This page describes the real, current API — one working endpoint, plus a roadmap of what's planned but not built. Nothing here requires a paid key or a hosted server; you run this yourself from the repo.
Documentation

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.

Building an agent rather than a program? The same data is served over Model Context Protocol, with seven tools, variable search and per-cell budgets instead of endpoints and request counts — see MCP for agents. You sign in there with the same account you use here.
v0.0.1 · pre-alpha · no published OpenAPI schema yet

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.

Tokens expire after 12 hours. The CLI writes one to ./.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
Coverage, today: the deployed Nominatim instance holds a national OpenStreetMap extract, and so do the tract, county, state and PUMA tables. The limit is speed rather than extent: only Georgia is reliably quick, and an address in another state may exceed the geocoder's time budget and return 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).

GET /address_data

Query parameters

NameTypeDescription
streetstringStreet address, house number included.
citystringCity name.
countystringCounty name.
statestringState name or abbreviation.
postalcodestringZIP code.
amenitystringNamed place, e.g. a building name.
variablesstringComma-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.

GET /demo/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.

POST /structured_geocode

Body parameters

NameTypeDescription
streetstringStreet address.
citystringCity name.
countystringCounty name.
statestringState name or abbreviation.
postalcodestringZIP code.
countrystringCountry 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.

GET /health

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": "…"}}.

StatusStageMeaning
400geocodeNot enough of an address to search for.
401—Missing, invalid, or expired bearer token.
404geocodeNo match for the address. Fix the address.
404tract_lookupThe match is outside any 2020 Census tract we hold.
404tract_dataThe address is fine; we have no attribute row for its tract. Retrying will not help.
422precisionThe match is coarser than street level, or outside the United States.
422variablesA name in variables matches no column.
429—Rate limit exceeded — 60 requests/minute per IP by default, less on the demo route.
503tract_lookup / tract_dataA 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 /fields endpoint 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.