API
The app's server function returns the OpenAQ open-data archive's hourly readings for one monitoring location and up to 93 days as one CSV. It is free and needs a SkillSafe app token for this app. The AQI, NowCast and outlook are computed in the page; the same formulas are listed under “How the numbers are made” in the app.
1. Get a token
Exchange your SkillSafe API key for an app token (valid 30 days). Requests with a guest token are refused.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/authorize \
-H "Authorization: Bearer YOUR_SKILLSAFE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug":"air-quality-outlook"}'
# -> {"ok":true,"data":{"token":"aut_...","expires_at":"..."}}
import requests
r = requests.post("https://api.skillsafe.ai/v1/app-api/authorize",
headers={"Authorization": "Bearer YOUR_SKILLSAFE_API_KEY"},
json={"slug": "air-quality-outlook"})
token = r.json()["data"]["token"]
const r = await fetch("https://api.skillsafe.ai/v1/app-api/authorize", {
method: "POST",
headers: { Authorization: "Bearer YOUR_SKILLSAFE_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ slug: "air-quality-outlook" }),
});
const token = (await r.json()).data.token;
2. Fetch hourly readings
GET https://air-quality-outlook.skillsafe.ai/api/days?location=ID&start=YYYY-MM-DD&end=YYYY-MM-DD
location: a numeric OpenAQ location id, for example270693(London, Waterloo Place). The app's list of about 14,000 locations with recent data is at stations.json ([id, name, lat, lon, pollutant bitmask, city index, km to city]; bits: pm25, pm10, o3, no2, so2, co).start,end: local dates, at most 93 days per request, from 2016 on. Days the archive does not have are skipped.- Response:
text/csv. A first line# location_id=…; name=…; lat=…; lon=…; days=…, thendatetime,parameter,units,value.datetimeis local time with its UTC offset and marks the end of the averaging period. Every parameter the location reports is included (also temperature, humidity and particle counts at some sites), in the provider's units.
curl -s "https://air-quality-outlook.skillsafe.ai/api/days?location=270693&start=2026-10-01&end=2026-10-31" \ -H "Authorization: Bearer YOUR_APP_TOKEN"
import csv, io, requests
r = requests.get("https://air-quality-outlook.skillsafe.ai/api/days",
params={"location": 270693, "start": "2026-10-01", "end": "2026-10-31"},
headers={"Authorization": "Bearer " + token})
lines = [l for l in r.text.splitlines() if not l.startswith("#")]
rows = list(csv.DictReader(lines))
const res = await fetch("https://air-quality-outlook.skillsafe.ai/api/days?location=270693&start=2026-10-01&end=2026-10-31",
{ headers: { Authorization: "Bearer " + token } });
const csvText = await res.text();
Errors
| Status | Meaning |
|---|---|
| 400 | bad_location or bad_range: check the id and the dates (93 days at most) |
| 401 | No token, a guest token, or an expired token |
| 404 | no_data: the archive has no files for that location in that range |
| 502 | The archive did not answer |
Data: OpenAQ open-data archive and its original providers; licences vary by provider. The archive is published about two to four days behind real time. Errors come back as {"error":{"code","message"}}.