# 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.

Get started → Explore the API
## 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. */}

Air quality

Enrich locations and routes with pollution concentrations, exposure (relative inhaled dose) and the banded AirTrack Clean Air Score.

Route planning

Submit a plan request and retrieve lower-exposure routes between two points, with a quota-aware asynchronous workflow.

Connections

Exchange a short-lived pairing code for a long-lived connection token. Listing and revoking connections happens in the AirTrack app.

## 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. */}

Connected device

A device that reads air quality for its owner — a display, wearable or sensor. The user pairs it from the AirTrack app.

Per-user integration

A service that enriches a specific user's data on their behalf. Paired by that user, the same way as a device.

B2B partner

A commercial integration scoring routes or enriching data at scale, under a partner agreement.

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.