Skip to content

API

Georgia data, for your code

A read-only REST API over every cleaned, standardized Georgia dataset. Pull rows as JSON, CSV, or Parquet, filtered by year, place, and demographics. No API key, no sign-up.

Base URL

https://georgiacivicdata.org/api/v1
  • Public & read-only. Every endpoint is a GET. There is nothing to authenticate.
  • Contract-driven. Each dataset is generated from a versioned ODCS data contract, so its columns, units, ranges, and allowed values are stable and documented.
  • Same data everywhere. The dashboard and MCP server read this same API.

Your first request

Fact data lives at /api/v1/{main_topic}/{topic}. Ask for 2024 composite ACT scores at the district level:

curl "https://georgiacivicdata.org/api/v1/education/act_scores?year=2024&test_component=composite&detail=districts"

JSON responses are an envelope: a data array of rows plus a pagination block. Dimension labels (district and school names) are joined in for you.

{
  "data": [
    {
      "year": 2024,
      "district_code": "601",
      "district_name": "Appling County",
      "test_component": "composite",
      "num_tested": 234,
      "avg_score": 18.7
    }
  ],
  "pagination": { "limit": 1000, "offset": 0, "total": 181, "returned": 181 }
}

Response formats

Add ?format= to choose how rows come back:

  • json (default). Paginated envelope with data + pagination.
  • csv. Raw CSV bytes with an X-Total-Count header.
  • parquet. Raw Parquet bytes — ideal for analysis in polars/DuckDB.

Filtering

  • Year. year=2024 (exact) or year_min / year_max (inclusive range). Use one or the other, not both.
  • Detail level. detail=states, districts, or schools — whichever the dataset publishes. One dataset documents all of its levels.
  • Geography & demographics. district_code, school_code, and demographic take comma-separated code lists.
  • Per-column filters. Every categorical column is its own parameter (e.g. test_component=composite). Comma-separated values are OR within a parameter; multiple parameters AND together.

Each dataset page lists its exact filter parameters and their allowed values.

Pagination

Use limit (default 1000, max 10000) and offset. The pagination.total field always reports the full match count so you can page through deterministically.

Errors & rate limits

Errors return { "error": { "code", "message", "details?" } } with a matching HTTP status — e.g. TOPIC_NOT_FOUND (404), INVALID_FILTER_VALUE (400), RATE_LIMITED (429). The API is generously rate-limited; back off on a 429 and retry.

Discovery

Don’t hard-code dataset shapes — discover them:

  • Catalog. GET /api/v1/datasets lists every dataset and dimension.
  • Schema. GET /api/v1/datasets/{main}/{topic} returns columns, filters, ranges, and allowed values as JSON.
  • Contract. .../contract?format=yaml returns the raw ODCS contract.

See the endpoint reference for every route, or browse the dataset catalog.

For AI agents

Point a coding agent at one of these — both are generated from the same contracts:

  • llms.txt An index of the API and every dataset.
  • llms-full.txt The dense full reference — every column, filter, enum, and range inline.
  • openapi.json OpenAPI schema (note: per-column categorical filters are generated per request and not enumerated there — use the contracts or llms-full.txt for those).