# AirTrack API
> Complete documentation for Large Language Models
---
## Document: Quickstart
Make your first authenticated AirTrack API call in minutes — with copy-paste examples in curl, Python and TypeScript.
URL: /quickstart
# Quickstart
# Quickstart
This guide takes you from a token to a working call. Every example is shown in
**curl**, **Python** and **TypeScript** — pick your tab and paste.
## Before you start
You'll need:
- An AirTrack **premium** subscription on the owning account (premium is checked on
every call).
- A **connection token** (`aapk_…`). See [Authentication](/authentication) for the
full pairing flow; the short version is to redeem a pairing code created in the
AirTrack app:
```bash
curl -X POST https://api.airawarelabs.com/v1/connections/token \
-H "Content-Type: application/json" \
-d '{ "code": "048213", "name": "My integration" }'
# → { "token": "aapk_pdn_…", "scopes": ["aq:read", "aq:routes", "routes:plan"], … }
```
Requests go to one base URL per environment:
| Environment | Base URL |
| ----------- | -------- |
| Production | `https://api.airawarelabs.com` |
| Development | `https://api.airawarelabs-dev.dev` |
## 1. Set up an authenticated client
Every request carries your token in the `Authorization` header. Set it up once and
reuse it for every call below.
```bash title="curl"
export AIRTRACK_BASE="https://api.airawarelabs.com"
export AIRTRACK_TOKEN="aapk_pdn_…"
```
```python title="Python"
import requests
BASE = "https://api.airawarelabs.com"
session = requests.Session()
session.headers["Authorization"] = "Bearer aapk_pdn_…"
```
```typescript title="TypeScript"
const BASE = "https://api.airawarelabs.com";
const TOKEN = process.env.AIRTRACK_TOKEN!; // aapk_pdn_…
async function airtrack(path: string, body?: unknown) {
const res = await fetch(`${BASE}${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return res.json();
}
```
## 2. Score a location
Get the instantaneous **Clean Air Score** (100 = cleanest, 0 = worst) for a point.
Requires the `aq:read` scope; costs **1 credit**.
```bash title="curl"
curl -X POST "$AIRTRACK_BASE/v1/airquality/score" \
-H "Authorization: Bearer $AIRTRACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "lat": 51.5074, "lon": -0.1278 }'
```
```python title="Python"
r = session.post(f"{BASE}/v1/airquality/score", json={"lat": 51.5074, "lon": -0.1278})
print(r.json()) # {'score': 82, 'ttl': 3600}
```
```typescript title="TypeScript"
const score = await airtrack("/v1/airquality/score", { lat: 51.5074, lon: -0.1278 });
console.log(score); // { score: 82, ttl: 3600 }
```
```json title="Response"
{ "score": 82, "ttl": 3600 }
```
`ttl` is the number of seconds until it's worth polling again — the underlying data
updates hourly, so there's no value in polling faster.
## 3. Forecast a day
Hourly Clean Air Scores and pollutant concentrations for a date at a location.
Requires `aq:read`; costs **2 credits**. Omit the coordinates to use the caller's
approximate IP location.
```bash title="curl"
curl -X POST "$AIRTRACK_BASE/v1/airquality/forecast" \
-H "Authorization: Bearer $AIRTRACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"latitude": 51.5074,
"longitude": -0.1278,
"date": "2026-07-08",
"timezone": "Europe/London"
}'
```
```python title="Python"
r = session.post(
f"{BASE}/v1/airquality/forecast",
json={
"latitude": 51.5074,
"longitude": -0.1278,
"date": "2026-07-08",
"timezone": "Europe/London",
},
)
print(r.json())
```
```typescript title="TypeScript"
const forecast = await airtrack("/v1/airquality/forecast", {
latitude: 51.5074,
longitude: -0.1278,
date: "2026-07-08",
timezone: "Europe/London",
});
console.log(forecast);
```
```json title="Response"
{
"location": "London, UK",
"timestamps": [1751972400, 1751976000],
"measures": {
"clean_air_score": [82.0, 79.0],
"pm25": [8.1, 9.4]
}
}
```
`timestamps` is a shared time axis (epoch seconds); each list in `measures` runs
parallel to it.
## 4. Enrich a route
Score a route you supply — per-waypoint measures plus a route-level exposure summary.
Requires the `aq:routes` scope; costs **1 credit per waypoint**. Provide a `time` on
every waypoint (a recorded track), or omit them all and set a `start_time` to have
the times estimated from the activity's speed.
```bash title="curl"
curl -X POST "$AIRTRACK_BASE/v1/airquality/routes" \
-H "Authorization: Bearer $AIRTRACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"waypoints": [
{ "lat": 51.5074, "lon": -0.1278, "time": "2026-07-08T08:00:00Z" },
{ "lat": 51.5121, "lon": -0.1236, "time": "2026-07-08T08:12:00Z" }
],
"activity_type": "walking",
"environment_type": "outdoor"
}'
```
```python title="Python"
r = session.post(
f"{BASE}/v1/airquality/routes",
json={
"waypoints": [
{"lat": 51.5074, "lon": -0.1278, "time": "2026-07-08T08:00:00Z"},
{"lat": 51.5121, "lon": -0.1236, "time": "2026-07-08T08:12:00Z"},
],
"activity_type": "walking",
"environment_type": "outdoor",
},
)
print(r.json())
```
```typescript title="TypeScript"
const enriched = await airtrack("/v1/airquality/routes", {
waypoints: [
{ lat: 51.5074, lon: -0.1278, time: "2026-07-08T08:00:00Z" },
{ lat: 51.5121, lon: -0.1236, time: "2026-07-08T08:12:00Z" },
],
activity_type: "walking",
environment_type: "outdoor",
});
console.log(enriched);
```
```json title="Response"
{
"waypoints": [[51.5074, -0.1278], [51.5121, -0.1236]],
"measures": {
"clean_air_score": [82.0, 78.0],
"pm25": [8.1, 9.6]
},
"summary": {
"clean_air_score": { "score": 80.0, "band": "5", "description": "Excellent", "colour": "#00C853" },
"inhaled_dose": 38.4,
"average_pollutants": { "pm25": 8.85 }
}
}
```
To **plan** a lower-exposure route (rather than score one you already have), see
[`POST /v1/routes`](/api/route-planning) in the API reference — it's an asynchronous
submit-then-poll workflow.
## 5. Check your remaining credits
Endpoints that compute air quality draw from one shared monthly **credit pool**, with a
smaller daily burst cap — see [Usage & credits](/credits) for the full cost table.
Reading your balance is itself free, as are redeeming a pairing code and polling for a
route-plan result. Any valid token works, no particular scope required.
```bash title="curl"
curl "$AIRTRACK_BASE/v1/quota" \
-H "Authorization: Bearer $AIRTRACK_TOKEN"
```
```python title="Python"
r = session.get(f"{BASE}/v1/quota")
print(r.json())
```
```typescript title="TypeScript"
const quota = await airtrack("/v1/quota");
console.log(quota);
```
```json title="Response"
{
"used": 128,
"limit": 1000,
"remaining": 872,
"daily_used": 40,
"daily_limit": 100,
"daily_remaining": 60
}
```
## Handling errors
Every handled error returns a JSON body with a single `detail` string, and rate
limits come back as `429` with a `Retry-After` header. See [Errors](/errors) for the
full status-code reference and a robust retry pattern in Python and TypeScript.
## Next steps
- Read the [Authentication](/authentication) guide for the full credential and scope
model.
- Browse the complete [API Reference](/api) for every endpoint, field and example.
- Point your tooling or AI assistant at /openapi.json or /llms-full.txt.
---
## Document: Introduction
Getting started with the AirTrack Enterprise API.
URL: /introduction
# Introduction
AirTrack Enterprise API
Air quality intelligence,
built into your product.
Programmatic access to AirTrack's route optimisation and air quality
calculation tools — the same engine that powers the AirTrack apps.
## What you can do
{/* Icons are Lucide (https://lucide.dev, ISC/MIT), inlined as monochrome SVG
using currentColor so they inherit the brand-orange accent. */}
## Base URLs
| Environment | Base URL |
| ----------- | -------- |
| Production | `https://api.airawarelabs.com` |
| Development | `https://api.airawarelabs-dev.dev` |
## Next steps
1. Follow the [Quickstart](/quickstart) to make your first call in minutes, with
copy-paste examples in curl, Python and TypeScript.
2. Read the [Authentication](/authentication) guide for the full credential and scope
model.
3. Explore the full [API Reference](/api) for every endpoint, schema and example.
## OpenAPI spec
{/* Raw anchors with target="_blank" so the browser fetches the static file
directly, rather than Zudoku's client-side router treating it as a page. */}
The complete machine-readable contract is published at /openapi.json — import it into your tooling, generate a client, or point an AI coding assistant at it. There's also an /llms-full.txt with these guide pages as a single file.
**Working with an AI assistant? Give it both files.** `llms-full.txt` contains the guides you're reading now — Introduction, Quickstart, Authentication and Errors — but *not* the endpoint reference, which is generated from the contract. `openapi.json` is the authoritative description of every endpoint, schema, parameter and error response.
Need access or have a question? Contact
[support@airawarelabs.com](mailto:support@airawarelabs.com).
---
## Document: Errors
The AirTrack API error envelope, status codes, and how to handle rate limits and credits.
URL: /errors
# Errors
# Errors
The AirTrack API uses conventional HTTP status codes and a single, predictable error
envelope, so you can handle failures the same way everywhere.
## The error envelope
Every handled error returns a JSON body with a single `detail` string describing what
went wrong:
```json
{ "detail": "Your token lacks the required scope" }
```
Every handled status uses this shape, including `422` — where `detail` is a fixed
`"Request validation failed"` for body validation, because the offending input is
deliberately not echoed back. The one exception is **`500`**, which returns the plain text
`Internal Server Error`.
## Status codes
| Status | Meaning | Body |
|--------|---------|------|
| `400` | Bad request — for example an invalid forecast date, a route that exceeds the maximum plan distance, or waypoint times outside the window air-quality data exists for. | `{ "detail": … }` |
| `401` | Missing, unknown or revoked token; or an invalid/expired pairing code. | `{ "detail": … }` |
| `403` | Your token isn't permitted to perform this operation — usually a missing scope, or the owning user no longer holding a premium plan. The exact causes vary per endpoint; see its own description. | `{ "detail": … }` |
| `404` | No such resource, or it belongs to a different AirTrack user — for example polling a route-plan `request_id` created by someone else. Plans are owned by the user, not the connection, so your other connections can poll them. | `{ "detail": … }` |
| `422` | The request failed validation — the body, or a path parameter such as an invalid `request_id`. | `{ "detail": … }` |
| `429` | Rate limited, or your credit pool (monthly or daily burst) is exhausted. | `{ "detail": … }` + `Retry-After` |
| `503` | An upstream data source was unavailable — the call was not charged. | `{ "detail": … }` |
## Rate limits & credits
Endpoints that compute air quality draw from one shared monthly **credit pool**, with a
smaller **daily burst cap** so the whole month can't be spent at once — costs per
endpoint are on the [Usage & credits](/credits) page. Redeeming a
pairing code, reading `GET /v1/quota` and polling for a route-plan result are free.
When either limit is reached, calls return `429` with a `Retry-After` header giving the
number of seconds to wait.
Check your current balance any time with [`GET /v1/quota`](/api/quota#get-quota).
Calls that fail **before delivering a result** — a `503` upstream outage, or a route
plan rejected for being too long — are **refunded automatically**, so a failed request
never costs you credits.
## Handling errors in code
Branch on the status code, honour `Retry-After` on a `429`, and read `detail` for a
human-readable reason. This helper retries rate-limited calls and raises on anything
else:
```python title="Python"
import time
import requests
def call_with_retry(session, method, url, *, attempts=3, **kwargs):
for _ in range(attempts):
r = session.request(method, url, **kwargs)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "1")))
continue
if not r.ok:
detail = r.json().get("detail", r.text) if "json" in r.headers.get(
"content-type", ""
) else r.text
raise RuntimeError(f"{r.status_code}: {detail}")
# 204 has no body — don't try to parse one.
return r.json() if r.status_code != 204 else None
raise RuntimeError("Rate limited — retries exhausted")
```
```typescript title="TypeScript"
async function callWithRetry(path: string, init: RequestInit, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(`${BASE}${path}`, init);
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? "1");
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(`${res.status}: ${body.detail ?? res.statusText}`);
}
// 204 has no body — don't try to parse one.
return res.status === 204 ? null : res.json();
}
throw new Error("Rate limited — retries exhausted");
}
```
For how to obtain and secure a token, see [Authentication](/authentication). For
runnable examples of each endpoint, see the [Quickstart](/quickstart).
---
## Document: Usage & credits
How the monthly credit pool, per-endpoint costs, burst cap and refunds work.
URL: /credits
# Usage & credits
# Usage & credits
Endpoints that compute air quality draw from a single monthly **credit pool** — one
balance you spend however you like across the API. There are no per-endpoint quotas:
a credit spent on a forecast is a credit not spent on route enrichment.
An AirTrack **Premium** subscription includes **1,000 credits per month**, with a
**daily burst cap of 100** (see below). The allowance belongs to the user, not the
token: every connection a user pairs draws from the same pool.
## What each call costs
Each endpoint costs credits roughly in proportion to the work it does:
| Endpoint | Credits per call |
|----------|------------------|
| Score a point | 1 |
| Forecast a day | 2 |
| Enrich a route | 1 per waypoint |
| Plan a route | 25 (flat) |
Checking your balance with [`GET /v1/quota`](/api/quota#get-quota) is free, as are
redeeming a pairing code and polling for a route-plan result.
## Daily burst cap
A smaller **daily burst cap** sits inside the monthly pool and stops the whole month
being spent at once. Both limits travel together: a call must fit within whatever
remains of each. [`GET /v1/quota`](/api/quota#get-quota) reports both balances.
## When a limit is reached
When either limit is exhausted, calls return `429` with a `Retry-After` header giving
the number of seconds until the relevant window resets. Back off until then rather
than retrying in a loop; the [errors guide](/errors) covers retry patterns.
## Refunds
Calls that fail before delivering a result are refunded automatically — for example a
`503` upstream outage, or a route plan the planner rejects. Validation failures
(`400`/`422`) are never charged in the first place.
---
## Document: Authentication
How to obtain a token and authenticate requests to the AirTrack API.
URL: /authentication
# Authentication
# Authentication
Every request to the AirTrack API is authenticated with a **bearer token**:
```http
Authorization: Bearer aapk_pdn_…
```
A credential is a **connection**. It carries a set of **scopes** (the capabilities
it's allowed to use) and is owned by either a **user** or a **partner**. How you
obtain a token depends on what you're building — but once you have one, every call
works the same way.
## Which integration are you building?
{/* Icons are Lucide (https://lucide.dev, ISC/MIT), inlined as monochrome SVG. */}
A **connected device** and a **per-user integration** both obtain a token through
the [pairing-code flow](#user-owned-connections). A **B2B partner** is
[issued a token directly](#partner-connections).
## Getting a token
### User-owned connections
For devices and per-user integrations. The owning user pairs your client from the
AirTrack app; the pairing grants it a set of scopes fixed by the server. This requires
the user to have an active AirTrack **premium** subscription.
1. **The user creates a pairing code.** In the AirTrack app, the user generates a
6-digit code granting the standard integration scopes — `aq:read`, `aq:routes`
and `routes:plan`. The code is valid for **60 seconds** and can be used once.
2. **The user enters the code into your client.**
3. **Your client redeems the code for a token.** No authentication is required —
the code is the proof. Supply a `name` to identify the connection:
```bash
curl -X POST https://api.airawarelabs.com/v1/connections/token \
-H "Content-Type: application/json" \
-d '{ "code": "048213", "name": "Living-room display" }'
# → { "connection_id": "…", "name": "Living-room display",
# "scopes": ["aq:read", "aq:routes", "routes:plan"], "token": "aapk_pdn_…" }
```
4. **Store the token securely** and use it for every subsequent call.
The scopes are fixed by the server when the code is minted — a client cannot request
or escalate its own scopes. Premium is re-checked on every call that computes air quality or
submits a route plan, so those start returning `403` if the user's subscription lapses.
Reading your quota and polling for a plan result are not premium-gated — a lapsed user can
still collect a result they already paid for.
See [`POST /v1/connections/token`](/api/connections#redeem-pairing-code) in the API
reference.
### Partner connections
For B2B integrations — scoring routes or enriching data at scale — tokens are
issued directly under a partner agreement rather than through pairing. Get in touch
at [support@airawarelabs.com](mailto:support@airawarelabs.com) to set up a partner
account. Your token carries the scopes agreed for your integration and stays valid
while your partner account is active.
## Authenticating requests
Send the token as a bearer header on **every** request:
```bash
curl -X POST https://api.airawarelabs.com/v1/airquality/score \
-H "Authorization: Bearer aapk_pdn_…" \
-H "Content-Type: application/json" \
-d '{ "lat": 51.5, "lon": -0.12 }'
# → { "score": 82, "ttl": 3600 } # Clean Air Score: 100 = cleanest, 0 = worst
```
Each endpoint requires a specific scope. If your token lacks it, the call returns
`403`. Tokens are environment-specific — the segment after `aapk_` (`pdn`, `tst`)
tells you which environment issued it.
## Scopes
Scopes name **capabilities**, not consumers. They're fixed when the connection is
created — by the server at pairing, or with a partner token — and can't be changed by
the client.
| Scope | Allows |
|-------|--------|
| `aq:read` | Read air quality for points and forecasts (current and historical). |
| `aq:routes` | Enrich a route you supply with per-waypoint air quality. |
| `routes:plan` | Plan lower-exposure routes between two points. |
## Token security
- The raw token is shown **once**, when the connection is created — store it
securely and never expose it in client-side web code. We keep only a hash.
- **Revoking** a connection (from the AirTrack app) invalidates its token
immediately — the next call returns `401`.
- To **rotate** a token, revoke the connection and pair again.
## Error responses
The two you'll meet most often when getting authentication right:
| Status | Meaning |
|--------|---------|
| `401` | Missing, unknown or revoked token; or an invalid/expired pairing code. |
| `403` | Your token isn't permitted to perform that operation — usually a missing scope; on the endpoints that compute air quality or submit a plan, also a lapsed premium plan. Causes vary per endpoint; see its own description. |
See [Errors](/errors) for the full status-code reference, the error envelope, and a
robust retry pattern for rate limits.