Skip to main content

Historical Data — Cities

Access the full history of calibrated air quality measurements for all sites within your city's Grid. Historical data is retrieved through the Analytics API using a POST request. Each individual request is capped at approximately 2 months of data — use batched requests with pagination to retrieve longer periods.

Tier requirement

Historical data access requires a Standard Tier subscription or above.

Grid ID direct filtering — coming soon

The Analytics API does not yet accept grid_id as a request parameter. You must supply individual Site IDs (sites) or device names (device_names) in the request body instead.

Workaround: Call the Metadata API (GET /api/v2/devices/grids/{GRID_ID}/generate) to retrieve all site and device identifiers for your Grid, then include those in your Analytics API request.

Direct Grid ID filtering will be added to the Analytics API in a future release.


Overview

To query historical data for your Grid, supply the Site IDs or device names for that Grid in the Analytics API request. Use the Metadata API (GET /api/v2/devices/grids/{GRID_ID}/generate) to retrieve all site and device identifiers for your Grid.


Endpoint

POST https://api.airqo.net/api/v3/public/analytics/data-download?token=YOUR_SECRET_TOKEN
Content-Type: application/json

Request parameters

ParameterTypeRequiredDescription
networkstringYesAlways "airqo"
startDateTimestringYesStart of range (ISO 8601)
endDateTimestringYesEnd of range (ISO 8601)
datatypestringYes"calibrated"
downloadTypestringYes"json"
frequencystringYes"hourly" or "daily"
sitesarrayNoFilter to specific Site IDs in your Grid
device_namesarrayNoFilter to specific device names
pollutantsarrayNoe.g. ["pm2_5", "pm10"]
metaDataFieldsarrayNoe.g. ["latitude", "longitude"]
weatherFieldsarrayNoe.g. ["temperature", "humidity"]
cursorstringNoPagination cursor from previous response

Example request — daily aggregated data for a city

curl -X POST "https://api.airqo.net/api/v3/public/analytics/data-download?token=YOUR_SECRET_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network": "airqo",
"datatype": "calibrated",
"downloadType": "json",
"startDateTime": "2025-01-01T00:00:00Z",
"endDateTime": "2025-02-28T23:59:59Z",
"sites": ["64f7b3e8c9d25a0013f2d456", "64f7b3e8c9d25a0013f2d789"],
"pollutants": ["pm2_5", "pm10"],
"metaDataFields": ["latitude", "longitude"],
"frequency": "daily"
}'

Python (with pagination):

import requests

token = 'YOUR_SECRET_TOKEN'

base_payload = {
"network": "airqo",
"datatype": "calibrated",
"downloadType": "json",
"startDateTime": "2025-01-01T00:00:00Z",
"endDateTime": "2025-02-28T23:59:59Z",
"sites": ["64f7b3e8c9d25a0013f2d456", "64f7b3e8c9d25a0013f2d789"],
"pollutants": ["pm2_5", "pm10"],
"metaDataFields": ["latitude", "longitude"],
"frequency": "daily"
}

all_records = []
cursor = None

while True:
payload = {**base_payload}
if cursor:
payload['cursor'] = cursor

response = requests.post(
f"https://api.airqo.net/api/v3/public/analytics/data-download?token={token}",
json=payload
).json()

all_records.extend(response['data'])

if not response['metadata']['has_more']:
break

cursor = response['metadata']['next']

print(f"Total records: {len(all_records)}")

JavaScript (async):

async function fetchAllCityHistory(siteIds, token) {
const basePayload = {
network: 'airqo',
datatype: 'calibrated',
downloadType: 'json',
startDateTime: '2025-01-01T00:00:00Z',
endDateTime: '2025-02-28T23:59:59Z',
sites: siteIds,
pollutants: ['pm2_5', 'pm10'],
metaDataFields: ['latitude', 'longitude'],
frequency: 'daily'
};

let allRecords = [];
let cursor = null;

do {
const payload = cursor ? { ...basePayload, cursor } : basePayload;

const response = await fetch(
`https://api.airqo.net/api/v3/public/analytics/data-download?token=${token}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
);

const data = await response.json();
allRecords = allRecords.concat(data.data);
cursor = data.metadata.has_more ? data.metadata.next : null;
} while (cursor);

return allRecords;
}

Example response

{
"status": "success",
"message": "Data downloaded successfully",
"data": [
{
"site_name": "Nairobi CBD",
"device_name": "airqo_k001",
"datetime": "2025-01-01 00:00:00Z",
"pm2_5": 18.7,
"pm10": 24.3,
"latitude": -1.2921,
"longitude": 36.8219,
"temperature": 21.4,
"humidity": 70.2,
"network": "airqo",
"frequency": "daily"
}
],
"metadata": {
"total_count": 2400,
"has_more": true,
"next": "eyJsYXN0SWQiOiI2NWM4Z..."
}
}

Choosing frequency

frequency valueDescriptionWhen to use
"hourly"One row per device per hourTime-series analysis, charts
"daily"One row per device per day (average)Monthly reports, trend summaries

Best practices

  • Break large date ranges into ~2-month batches — each request is capped at approximately 2 months of data. Query in sequential batches and combine the results to cover longer periods.
  • Filter to only the sites you need — specifying sites reduces the data returned.
  • Cache your results — historical data does not change, so you can store it locally and avoid re-querying.