GeoHealth Context API
Census-tract-level geographic health intelligence API. Given a street address or lat/lng coordinates, returns demographics, CDC/ATSDR Social Vulnerability Index (SVI) themes, CDC PLACES health outcome measures, and an optional AI-generated narrative for the surrounding census tract.
Features
- Address and coordinate geocoding (Census Bureau + Nominatim fallback)
- Census tract resolution via PostGIS spatial queries or FIPS-code lookup
- ACS demographics: population, income, poverty, insurance, unemployment, age
- CDC/ATSDR SVI theme percentile rankings (4 themes)
- CDC PLACES health outcome measures (crude prevalence)
- Composite SDOH index
- AI-generated narrative summaries (Anthropic Claude)
- Batch address lookups, spatial radius search, tract comparison
- Per-key sliding-window rate limiting with standard headers
- SHA-256 API key hashing
Quick Start
1. Clone and start services
git clone https://github.com/RussellStover1983/geohealth-api.git
cd geohealth-api
docker compose up --build -d
This launches PostgreSQL/PostGIS and the API on http://localhost:8000.
2. Load census tract data
# Single state (fast, good for development)
pip install -e ".[etl]"
python -m geohealth.etl.load_all --state 27
# All states (resumable)
python -m geohealth.etl.load_all --state all --resume
3. Verify
curl http://localhost:8000/health
# {"status":"ok","database":"connected","detail":null}
Interactive API Docs
Once running, explore the full API interactively:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
Authentication
Most endpoints require an API key via the X-API-Key header.
curl -H "X-API-Key: your-key-here" "http://localhost:8000/v1/context?address=..."
Configuration:
| Variable | Description |
|---|---|
AUTH_ENABLED |
Set to true to enforce key validation (default: false) |
API_KEYS |
Comma-separated list of valid keys |
Keys can be provided as plaintext or as pre-hashed SHA-256 hex strings. The API always hashes keys with SHA-256 before comparison — plaintext keys are never stored or logged.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | API and database connectivity check |
| GET | /v1/context |
Yes | Primary lookup — address or lat/lng to tract data |
| POST | /v1/batch |
Yes | Batch address lookup (up to BATCH_MAX_SIZE) |
| GET | /v1/nearby |
Yes | Find census tracts within a radius |
| GET | /v1/compare |
Yes | Compare two tracts or tract vs. averages |
| GET | /v1/dictionary |
Yes | Data dictionary — field definitions with clinical context |
| GET | /v1/stats |
Yes | Per-state data loading statistics |
| GET | /metrics |
No | Application metrics (counters, latency, cache stats) |
| GET | /llms.txt |
No | Agent-readable API overview (llmstxt.org) |
| GET | /llms-full.txt |
No | Full agent-readable reference with clinical context |
Example Requests
Context lookup by address
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/context?address=1234+Main+St,+Minneapolis,+MN+55401"
Context lookup by coordinates with narrative
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/context?lat=44.9778&lng=-93.265&narrative=true"
Batch lookup
curl -X POST -H "X-API-Key: test" -H "Content-Type: application/json" \
-d '{"addresses":["1234 Main St, Minneapolis, MN","456 Oak Ave, St Paul, MN"]}' \
http://localhost:8000/v1/batch
Nearby tracts
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/nearby?lat=44.9778&lng=-93.265&radius=5&limit=10"
Compare tracts
# Two tracts
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/compare?geoid1=27053026200&geoid2=27053026300"
# Tract vs. state average
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/compare?geoid1=27053026200&compare_to=state"
Statistics
curl -H "X-API-Key: test" "http://localhost:8000/v1/stats"
Python SDK
The project includes a typed Python SDK that wraps every endpoint with both async and sync clients.
Async usage
from geohealth.sdk import AsyncGeoHealthClient
async with AsyncGeoHealthClient("http://localhost:8000", api_key="your-key") as client:
result = await client.context(address="1234 Main St, Minneapolis, MN 55401")
print(result.tract.geoid, result.tract.poverty_rate)
nearby = await client.nearby(lat=44.9778, lng=-93.265, radius=3.0)
for tract in nearby.tracts:
print(tract.geoid, tract.distance_miles)
Sync usage
from geohealth.sdk import GeoHealthClient
with GeoHealthClient("http://localhost:8000", api_key="your-key") as client:
result = client.context(lat=44.9778, lng=-93.265)
print(result.tract.geoid)
Error handling
from geohealth.sdk import AsyncGeoHealthClient, RateLimitError, AuthenticationError
async with AsyncGeoHealthClient("http://localhost:8000", api_key="your-key") as client:
try:
result = await client.context(address="123 Main St")
except RateLimitError as exc:
print(f"Rate limited! Resets in {exc.rate_limit_info.reset}s")
except AuthenticationError as exc:
print(f"Auth failed: {exc.detail}")
MCP Server (for AI Agents)
The GeoHealth MCP server exposes all API endpoints as native tools for Claude Desktop, Claude Code, and other MCP-compatible agents. No HTTP wiring needed — agents call tools directly.
Install and run
pip install geohealth-api[mcp]
GEOHEALTH_API_KEY=your-key python -m geohealth.mcp
Claude Desktop configuration
Add to your claude_desktop_config.json:
{
"mcpServers": {
"geohealth": {
"command": "python",
"args": ["-m", "geohealth.mcp"],
"env": {
"GEOHEALTH_BASE_URL": "https://geohealth-api-production.up.railway.app",
"GEOHEALTH_API_KEY": "your-api-key"
}
}
}
}
Available tools
| Tool | Description |
|---|---|
lookup_health_context |
Primary lookup — address/coords to tract demographics, SVI, PLACES |
batch_health_lookup |
Multi-address lookup (up to 50) |
find_nearby_tracts |
Spatial radius search |
compare_tracts |
Compare tracts or tract vs averages |
get_data_dictionary |
Field definitions with clinical interpretation |
get_tract_statistics |
Data coverage by state |
Rate Limiting
Every authenticated response includes rate-limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per window |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Seconds until the window resets |
When the limit is exceeded, the API returns 429 Too Many Requests with the same headers. Configure via RATE_LIMIT_PER_MINUTE (default: 60).
Environment Variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string (postgresql+asyncpg://...) |
AUTH_ENABLED |
false |
Enable API key authentication |
API_KEYS |
— | Comma-separated valid keys (plaintext or SHA-256 hex) |
CORS_ORIGINS |
* |
Comma-separated allowed origins |
RATE_LIMIT_PER_MINUTE |
60 |
Max requests per key per minute |
ANTHROPIC_API_KEY |
— | Anthropic API key for narrative generation |
BATCH_MAX_SIZE |
50 |
Maximum addresses per batch request |
CACHE_MAXSIZE |
4096 |
LRU cache maximum entries |
CACHE_TTL |
3600 |
Cache time-to-live in seconds |
RUN_MIGRATIONS |
true |
Run Alembic migrations on startup |
LOG_FORMAT |
text |
text for human-readable, json for structured JSON |
LOG_LEVEL |
INFO |
Standard Python log levels |
Observability
Structured Logging
The API supports two log formats controlled by LOG_FORMAT:
text(default) — Human-readable format with optional request ID prefix:2024-01-15 12:00:00 INFO [abcdef123456] geohealth.access - GET /health 200 1.23msjson— Single-line JSON for log aggregators withtimestamp,level,logger,message,request_id, and optionalexceptionfields
Request Correlation
Every request gets an X-Request-ID header. If the client sends one, it is echoed back; otherwise a new UUID is generated. The request ID is included in all log lines and is available to downstream services for distributed tracing.
Application Metrics
GET /metrics returns a JSON snapshot of application metrics:
- Request counters — total requests, status code breakdown
- Cache stats — hits, misses, hit rate, current size
- Geocoder stats — Census/Nominatim success and failure counts
- Narrative stats — success and failure counts
- Latency percentiles — p50, p90, p95, p99
Enhanced Health Check
GET /health now includes cache, rate limiter, and uptime information when the database is healthy.
Deployment (Railway)
The API is configured for deployment on Railway using the existing multi-stage Dockerfile.
Prerequisites
- A Railway account
- The GitHub repo linked to your Railway project
1. Create a Railway project
- In the Railway dashboard, click New Project → Deploy from GitHub Repo
- Select the
geohealth-apirepository - Railway will detect the
railway.tomland build from the Dockerfile automatically
2. Add a PostGIS database service
Railway's managed Postgres does not include PostGIS binaries, so add a custom Docker service:
- In your Railway project, click New → Docker Image
- Set the image to
postgis/postgis:16-3.4 - Add a persistent volume mounted at
/var/lib/postgresql/data - Add these environment variables to the PostGIS service:
POSTGRES_USER=geohealthPOSTGRES_PASSWORD=<strong-password>POSTGRES_DB=geohealth
- Note the service's internal hostname (e.g.,
postgis.railway.internal) and port
3. Configure environment variables on the API service
Set these variables on the API service (not the PostGIS service):
| Variable | Value |
|---|---|
DATABASE_URL |
postgresql+asyncpg://geohealth:<password>@${{PostGIS.RAILWAY_PRIVATE_DOMAIN}}:5432/geohealth |
DATABASE_URL_SYNC |
postgresql://geohealth:<password>@${{PostGIS.RAILWAY_PRIVATE_DOMAIN}}:5432/geohealth |
AUTH_ENABLED |
true |
API_KEYS |
Comma-separated SHA-256 hashes (see step 6) |
CORS_ORIGINS |
Your frontend domain(s), e.g., https://app.example.com |
LOG_FORMAT |
json |
ANTHROPIC_API_KEY |
(optional) Your Anthropic key for AI narrative generation |
4. Deploy and verify
Railway auto-deploys on push to master. After the build completes:
curl https://<your-app>.railway.app/health
# {"status":"ok","database":"connected","detail":null}
5. Load census data
Run the ETL from your local machine against Railway's public database URL (found in the PostGIS service's networking settings):
pip install -e ".[etl]"
DATABASE_URL_SYNC=postgresql://geohealth:<password>@<public-host>:<port>/geohealth \
python -m geohealth.etl.load_all --state 27
For all states (resumable):
DATABASE_URL_SYNC=postgresql://geohealth:<password>@<public-host>:<port>/geohealth \
python -m geohealth.etl.load_all --state all --resume
6. Generate and distribute API keys
Generate a random key and its SHA-256 hash:
python -c "
import secrets, hashlib
key = secrets.token_urlsafe(32)
hashed = hashlib.sha256(key.encode()).hexdigest()
print(f'Give to client: {key}')
print(f'Add to API_KEYS: {hashed}')
"
Add the hash to API_KEYS in Railway's environment variables (comma-separated if multiple). Distribute the plaintext key to the API consumer — the API never stores or logs it.
Development
Local setup
pip install -e ".[dev]" # Core + test deps
pip install -e ".[dev,etl]" # Include ETL deps (geopandas, shapely, etc.)
Run the dev server
docker compose up -d db # Start PostGIS only
uvicorn geohealth.api.main:app --reload
Tests
pytest # All tests (no live DB required)
pytest tests/test_context.py -v # Single module
pytest -k test_auth # Pattern match
Lint
ruff check geohealth/ tests/ # line-length=99, target py311
Migrations
alembic upgrade head # Apply all
alembic revision --autogenerate -m "description" # Generate new
alembic stamp head # Mark existing DB as current
Project Structure
geohealth/
├── api/ # FastAPI app, routes, schemas, middleware, auth
│ └── routes/ # Endpoint modules (context, batch, nearby, compare, stats)
├── services/ # Geocoding, tract lookup, cache, rate limiter, narrator
├── etl/ # Data loading (TIGER/Line, ACS, SVI, PLACES)
├── db/ # SQLAlchemy models and async session
├── migrations/ # Alembic migration versions
└── config.py # Settings via pydantic-settings
License
MIT
Release files for geohealth-api 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| geohealth_api-0.1.0.tar.gz | 75.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| geohealth_api-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 147.6 kB
Release files / geohealth_api-0.1.0.tar.gz
| Download URL | geohealth_api-0.1.0.tar.gz |
|---|---|
| Size | 75.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e2ecd2265e6d1cfeee00c3abc51291a1228a0f54825b5ce0094b74e7453106e
|
|
BLAKE2b-256 checksum How to use checksums |
5f110d8dd624baaf1f11b93004ddd5c83f79e6317446180ef20bc417d903eccc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.10
|
Release files / geohealth_api-0.1.0-py3-none-any.whl
| Download URL | geohealth_api-0.1.0-py3-none-any.whl |
|---|---|
| Size | 71.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a63f9f5201e41b0d83c50cbef2c6c3946b3277df4ced998c44347a22e140daf8
|
|
BLAKE2b-256 checksum How to use checksums |
16a5b58d48dbcf51747e333af5eec06d630f9b6dafef62c21043b348dd43f16d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.10
|