openaqi docs

Current conditions

GET /v1/air — what the air is like at a coordinate, right now.

GET/v1/air

The 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

NameTypeDefaultNotes
lat, lonnumberRequired unless cell is given.
cellstringH3 resolution-7 index, if you already have one.
windowinteger60Minutes to average over. 5–10080.
max_ringsinteger2How far to widen if the cell is empty. 0 disables it.
metricsstringallComma-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);