History
GET /v1/history — a time series, aligned by timestamp.
/v1/historycurl -H "Authorization: Bearer $OPENAQI_KEY" \
"https://openaqi.net/api/v1/history?lat=37.3352&lon=-121.8811&hours=48&interval=1h"Parameters
| Name | Type | Default | Notes |
|---|---|---|---|
lat, lon | number | — | Required unless cell is given. |
cell | string | — | Taken literally — no ring widening. |
hours | integer | 24 | How far back, when from/to are absent. Max 2160. |
from, to | ISO-8601 | — | An exact range. |
interval | enum | derived | 1m 5m 15m 1h 3h 6h 1d. |
metrics | string | four | Comma-separated. |
format | enum | json | csv 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.csvEmpty cells for missing buckets, same as null in the JSON.