openaqi docs

History

GET /v1/history — a time series, aligned by timestamp.

GET/v1/history
curl -H "Authorization: Bearer $OPENAQI_KEY" \
  "https://openaqi.net/api/v1/history?lat=37.3352&lon=-121.8811&hours=48&interval=1h"

Parameters

NameTypeDefaultNotes
lat, lonnumberRequired unless cell is given.
cellstringTaken literally — no ring widening.
hoursinteger24How far back, when from/to are absent. Max 2160.
from, toISO-8601An exact range.
intervalenumderived1m 5m 15m 1h 3h 6h 1d.
metricsstringfourComma-separated.
formatenumjsoncsv returns text/csv.

The shape

Rows of {t, <metric>: value}, aligned by timestamp:

"points": [
  { "t": "2026-08-01T09:00:00.000Z", "pm2p5": 8.4, "co2": 612 },
  { "t": "2026-08-01T10:00:00.000Z", "pm2p5": null, "co2": 598 }
]

One row per bucket rather than one series per metric, because every consumer — a chart library, a dataframe, a spreadsheet — wants it aligned. Doing it here means it is done once instead of slightly differently by everyone.

Nulls are gaps, not zeros

A bucket with no readings is null. Never zero, never interpolated. Plot it as a gap — a zero would be a claim that the air was perfectly clean at a moment nobody was measuring.

Intervals

Omit interval and it is derived from your range so a 90-day request does not try to return two million rows.

Ask for one and it is honoured, up to the point where it would exceed 5,000 points. Past that it is widened, and the response says so:

"meta": {
  "interval_note": "The requested interval would have returned more than 5000 points, so it was widened to 3600s."
}

Silently returning a different resolution than asked for is how charts end up lying about their own precision.

CSV

curl -H "Authorization: Bearer $OPENAQI_KEY" \
  "https://openaqi.net/api/v1/history?lat=37.3&lon=-121.9&hours=168&format=csv" \
  -o air.csv

Empty cells for missing buckets, same as null in the JSON.