India Air Quality APIXKDR Forum

XKDR Forum · Open data

Hourly air quality for India, since 2009.

Readings from every station in CPCB's continuous monitoring network and the five US Embassy monitors, cleaned into one table. Query a slice through the API, or take the Parquet files whole.

Try it right now with the demo key aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe. The API examples on this page use it. It is capped at 10,000 rows per query and can only read the 2024 bulk files; a key of your own has no limits.

196.5 Mhourly readings
558monitoring stations
15pollutants
Jan 2009 – Mar 2026coverage
30 Sept 2026last published

Two ways in

Query API

Ask for exactly the slice you need

Filter by station, city, state, pollutant and dates. Aggregate to hourly, daily or monthly means. Get JSON, CSV or Parquet back.

curl
curl -H "Authorization: Bearer aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe" \
  "https://airquality.xkdr.org/v1/measurements?city=Delhi&parameter=PM2.5&start=2024-11-01&end=2024-11-30&agg=daily&format=csv"
Bulk Parquet

Or take the whole thing

One sorted Parquet file per month, under half a gigabyte in total. Download them, or point DuckDB at the URLs and query in place.

DuckDB
CREATE SECRET aqi (TYPE http, BEARER_TOKEN 'aqi_…');
SELECT station_id, avg(value) FROM read_parquet(
  'https://airquality.xkdr.org/v1/files/v1/measurements/year=2024/month=11/data.parquet')
WHERE parameter_name = 'PM2.5' GROUP BY 1;

Reference

Endpoints

Every request carries your key as a bearer token. All endpoints accept format=json|csv|parquet. Interactive documentation with a try-it-out console lives at /v1/docs.

EndpointWhat you get
GET /v1/metaWhat is published: export time, months covered, row counts, schema, your tier and row cap.
GET /v1/stationsStations with coordinates, first and last reading, and the pollutants each reports. Filters: source, state, city, q.
GET /v1/parametersPollutants, units, station counts.
GET /v1/measurementsThe time series. Parameters below.
GET /v1/filesList of bulk files with sizes and row counts.
GET /v1/files/<key>One bulk file. Honours Range, so query engines can read it in place.

Measurements parameters

NameMeaning
stationA station_id such as site_103 or DS1010001. Repeat for several.
parameterPM2.5, PM10, NO2, SO2, CO, Ozone, NH3, Benzene, … Repeat for several.
city, stateCase-insensitive exact match, e.g. city=Delhi.
sourcecpcb_caaqm or us_embassy.
start, endYYYY-MM-DD, inclusive, Indian Standard Time. Default: the last 30 days.
aggraw (default), hourly, daily, monthly. Aggregates return mean, min, max and count per period.
limitOptional row cap for this call.

Responses carry X-Row-Count and X-Truncated headers; the latter is only ever true when you passed limit.

Examples

Start in a notebook

Google Colab

Quickstart notebook

Runs top to bottom in about a minute with the demo key: coverage, a station map, one year of data pulled into the runtime, Delhi's November smog hour by hour, five cities through the year, the daily cycle, and maps of PM2.5 by station.

Everything in one repository

xKDR/Air-Quality-Database holds the notebook, this site and gateway, the query service, the Parquet exporter and the operator docs. Issues and pull requests welcome.

Three ways to ask

Python
import pandas as pd, requests

r = requests.get("https://airquality.xkdr.org/v1/measurements",
                 headers={"Authorization": "Bearer aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe"},
                 params={"station": "site_103", "parameter": ["PM2.5", "PM10"],
                         "start": "2024-01-01", "end": "2024-12-31", "agg": "daily", "format": "csv"})
df = pd.read_csv(io.StringIO(r.text), parse_dates=["period_start"])
R
library(httr2)
resp <- request("https://airquality.xkdr.org/v1/measurements") |>
  req_headers(Authorization = "Bearer aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe") |>
  req_url_query(city = "Mumbai", parameter = "PM2.5", start = "2024-11-01", end = "2024-11-30", agg = "daily", format = "csv") |>
  req_perform()
df <- read.csv(text = resp_body_string(resp))
DuckDB, a whole year in place
INSTALL httpfs; LOAD httpfs;
CREATE SECRET aqi (TYPE http, BEARER_TOKEN 'aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe');   -- the demo key can read the 2024 files
SELECT city_name, month, round(avg(value), 1) AS pm25
FROM read_parquet(
  list_transform(range(1, 13), m -> format('https://airquality.xkdr.org/v1/files/v1/measurements/year=2024/month={:02d}/data.parquet', m)),
  hive_partitioning = true)
WHERE parameter_name = 'PM2.5' AND city_name = 'Delhi' GROUP BY ALL ORDER BY ALL;
-- Wildcards don't work over HTTPS, so list the month files explicitly as above (or download them first).

Bulk data

The files

GET /v1/files lists everything. Keys look like v1/measurements/year=2024/month=03/data.parquet, plus v1/stations.parquet and v1/parameters.parquet.

Each month file is sorted by station, pollutant and time, with one-million-row row groups, so engines that read Parquet statistics skip most of a file when you filter on a station. Bulk files have no row limit.

Mirror everything
B=https://airquality.xkdr.org; K=aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe
for k in $(curl -s -H "Authorization: Bearer $K" $B/v1/files | jq -r '.files[].key'); do
  mkdir -p "$(dirname "$k")"
  curl -s -H "Authorization: Bearer $K" -o "$k" "$B/v1/files/$k"
done

Keys

Free keys, in about a minute.

  1. Fill in the short form. Your name and email; your organisation and plans are optional.
  2. Pass Cloudflare's human check. It usually ticks itself.
  3. Copy your key. It is shown once. Keep it private; if you lose it, sign up again for a new one.

No rate limits, no row caps

Every key can do everything the API offers, at whatever pace you need, and reads every bulk file. Two practical notes: JSON is verbose, so ask for format=csv or parquet when you expect more than a few hundred thousand rows, and single queries time out after 90 seconds, so pull whole years from the bulk files instead.

About the data

What you should know before you use it

Time

collected_at is a naive timestamp in Indian Standard Time. CPCB readings sit on the hour, embassy readings at half past. Aggregations use IST days and months.

Values

Published as received from the source networks: no gap filling, no outlier removal, no calibration. Units are harmonised in spelling only (µg/m³, mg/m³, ppb).

Stations

station_id is CPCB's site number (site_103) or the embassy id (DS1010001). A few dozen decommissioned stations are missing from CPCB's current list and carry no coordinates.

Licence and attribution. The compilation is released under Creative Commons Attribution 4.0: use it for anything, including commercial work, and credit India Air Quality Database, XKDR Forum together with the sources, the Central Pollution Control Board's Continuous Ambient Air Quality Monitoring network and the US Department of State via AirNow. If you build a product on it, we would appreciate hearing about it. How to cite it.

No warranty. The data is provided as is, without warranty of any kind. It is published as received from the monitoring networks, which themselves label readings preliminary and not fully validated, and it has not been validated by XKDR for regulatory, legal or health decisions. Use it at your own risk and check against the source for anything that matters.

Cite

How to cite the data

If you use the data in a paper, report, story or product, cite the database and credit its two sources.

Citation
XKDR Forum (2026). India Air Quality Database. https://airquality.xkdr.org

Sources: Central Pollution Control Board (CPCB), Continuous Ambient Air Quality Monitoring network; US Department of State air quality monitors, via AirNow.

BibTeX
@misc{xkdr_aqi_2026,
  author = {{XKDR Forum}},
  title  = {India Air Quality Database},
  year   = {2026},
  url    = {https://airquality.xkdr.org},
  note   = {Hourly readings, CPCB CAAQM
            and US Embassy monitors}
}