openaqi docs

Quickstart

A working request in under two minutes, in curl, JavaScript or Python.

1. Get a key

Create one at your account. Free, instant, no card.

Read keys start with oaq_ and can only read. A key that ships in a browser bundle can never forge readings, because sending data uses a different scope entirely.

2. Ask about a place

curl -H "Authorization: Bearer $OPENAQI_KEY" \
  "https://openaqi.net/api/v1/air?lat=37.3352&lon=-121.8811"
import { createClient } from "openaqi";

const openaqi = createClient({ apiKey: process.env.OPENAQI_KEY });
const { data } = await openaqi.air({ lat: 37.3352, lon: -121.8811 });

for (const m of data.metrics) {
  console.log(`${m.label}: ${m.value} ${m.unit} (${m.severity ?? "ungraded"})`);
}
import requests

r = requests.get(
    "https://openaqi.net/api/v1/air",
    params={"lat": 37.3352, "lon": -121.8811},
    headers={"Authorization": f"Bearer {key}"},
)
for m in r.json()["data"]["metrics"]:
    print(m["label"], m["value"], m["unit"], m["severity"])
req, _ := http.NewRequest("GET",
    "https://openaqi.net/api/v1/air?lat=37.3352&lon=-121.8811", nil)
req.Header.Set("Authorization", "Bearer "+key)

res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()

3. Read the answer

{
  "data": {
    "cell": "608693241486770175",
    "centre": { "lat": 37.3268, "lon": -121.9262 },
    "coverage": { "exact": true, "rings": 0, "approx_km": 0, "sensors": 3 },
    "metrics": [
      {
        "metric": "pm2p5",
        "value": 8.1,
        "unit": "µg/m³",
        "severity": "good",
        "threshold_source": "US EPA 24-hour PM2.5 breakpoints",
        "updated_at": "2026-08-02T09:14:00.000Z",
        "sensors": 3
      }
    ]
  },
  "meta": { "notice": "Unvalidated readings from consumer-grade sensors…" }
}

Three things to notice:

  • severity may be null. Where no published standard exists we say so rather than inventing bands.
  • threshold_source names the standard the grade came from. Show it.
  • coverage.exact tells you whether this is the cell you asked about.

4. Handle the boring cases

Cache responses. Current conditions carry max-age=60 and the data only moves every minute or two — respecting it means most apps never come near the rate limit.

Next: authentication if you are shipping to a browser, or errors for what to do when it does not work.