Current conditions
GET /v1/air — what the air is like at a coordinate, right now.
/v1/airThe endpoint everything else is arranged around: someone has a coordinate and wants to know what the air is like there.
curl -H "Authorization: Bearer $OPENAQI_KEY" \
"https://openaqi.net/api/v1/air?lat=37.3352&lon=-121.8811"Parameters
| Name | Type | Default | Notes |
|---|---|---|---|
lat, lon | number | — | Required unless cell is given. |
cell | string | — | H3 resolution-7 index, if you already have one. |
window | integer | 60 | Minutes to average over. 5–10080. |
max_rings | integer | 2 | How far to widen if the cell is empty. 0 disables it. |
metrics | string | all | Comma-separated subset, e.g. pm2p5,co2. |
Coverage, and why it matters
Cells are about 5 km across and coverage is a mosaic. A coordinate in a well-covered city can still land in a cell nobody reports from.
Rather than returning nothing useful, the search widens by grid ring — and says exactly how far it went:
"coverage": { "exact": false, "rings": 1, "approx_km": 2.4, "sensors": 3 }Check coverage.exact before attributing a reading to a specific address. When
it is false the number describes a neighbouring area, which is usually fine
and occasionally very much not.
One cell answers — never a blend. Averaging a cell with its neighbours would produce a number that is true of nowhere. Within the nearest ring the best-observed cell wins: most sensors first, then most samples.
Set max_rings=0 if you want strictness, and handle
no_coverage.
The response
{
"data": {
"cell": "608693241486770175",
"centre": { "lat": 37.3268, "lon": -121.9262 },
"coverage": { "exact": true, "rings": 0, "approx_km": 0, "sensors": 3 },
"window_minutes": 60,
"metrics": [
{
"metric": "pm2p5",
"label": "PM2.5",
"unit": "µg/m³",
"value": 8.1,
"latest": 7.4,
"min": 5.0,
"max": 12.2,
"samples": 42,
"sensors": 3,
"updated_at": "2026-08-02T09:14:00.000Z",
"severity": "good",
"threshold_source": "US EPA 24-hour PM2.5 breakpoints"
}
]
},
"meta": { "…": "…" }
}value versus latest
value is the mean over your window; latest is the single most recent
reading. They diverge during an event, and the divergence is the interesting
part — a latest well above value means something is happening now.
severity may be null
null means no published standard exists for that metric, so we decline to
invent one rather than shipping a colour we cannot source. threshold_source
names the standard when there is one. See metrics.
updated_at is per metric
Not every sensor reports every metric, and those that do report them at different rates — PM every 30 seconds, CO₂ only when its sensor finishes warming up. One timestamp for the whole response would imply a uniformity the data does not have.
Fetching one number
const pm = await openaqi.reading("pm2p5", { lat, lon });
if (pm && pm.severity !== "good") warn(pm.value, pm.unit);