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
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| geohealth_api-0.1.0.tar.gz | 75.9 kB | Without provenance | Details for geohealth_api-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| geohealth_api-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for geohealth_api-0.1.0-py3-none-any.whl |
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
|
Security for geohealth-api 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.