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.
Two ways in
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 -H "Authorization: Bearer aqi_demo_wbf92Qx21zX-Wa_Tg8Dx1nXe" \
"https://airquality.xkdr.org/v1/measurements?city=Delhi¶meter=PM2.5&start=2024-11-01&end=2024-11-30&agg=daily&format=csv"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.
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.
| Endpoint | What you get |
|---|---|
GET /v1/meta | What is published: export time, months covered, row counts, schema, your tier and row cap. |
GET /v1/stations | Stations with coordinates, first and last reading, and the pollutants each reports. Filters: source, state, city, q. |
GET /v1/parameters | Pollutants, units, station counts. |
GET /v1/measurements | The time series. Parameters below. |
GET /v1/files | List 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
| Name | Meaning |
|---|---|
station | A station_id such as site_103 or DS1010001. Repeat for several. |
parameter | PM2.5, PM10, NO2, SO2, CO, Ozone, NH3, Benzene, … Repeat for several. |
city, state | Case-insensitive exact match, e.g. city=Delhi. |
source | cpcb_caaqm or us_embassy. |
start, end | YYYY-MM-DD, inclusive, Indian Standard Time. Default: the last 30 days. |
agg | raw (default), hourly, daily, monthly. Aggregates return mean, min, max and count per period. |
limit | Optional 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
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
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"])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))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.
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"
doneKeys
Free keys, in about a minute.
- Fill in the short form. Your name and email; your organisation and plans are optional.
- Pass Cloudflare's human check. It usually ticks itself.
- 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.
XKDR Forum (2026). India Air Quality Database. https://airquality.xkdr.orgSources: Central Pollution Control Board (CPCB), Continuous Ambient Air Quality Monitoring network; US Department of State air quality monitors, via AirNow.
@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}
}