{
  "openapi": "3.1.0",
  "info": {
    "title": "AirTrack",
    "summary": "AirTrack API",
    "description": "Programmatic access to AirTrack's air-quality intelligence \u2014 score a point, forecast a day, or enrich and plan routes. Every endpoint except pairing-code redemption is authenticated with a connection bearer token (`Authorization: Bearer aapk_\u2026`). Endpoints that compute air quality draw from a shared monthly credit pool; redeeming a pairing code, reading `GET /v1/quota` and polling for a route-plan result are all free. See the Authentication guide to obtain a token.",
    "termsOfService": "https://www.airawarelabs.com/policy/terms-of-use",
    "contact": {
      "name": "Support",
      "email": "support@airawarelabs.com"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.airawarelabs.com",
      "description": "Production server"
    },
    {
      "url": "https://api.airawarelabs-dev.dev",
      "description": "Development server"
    }
  ],
  "paths": {
    "/v1/connections/token": {
      "post": {
        "tags": [
          "connections"
        ],
        "summary": "Redeem Pairing Code",
        "description": "Redeem a pairing code for a connection token. Unauthenticated \u2014 the code is the\nproof of ownership. The raw token is returned exactly once.",
        "operationId": "redeemPairingCode",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedeemPairingCodeSchema"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionTokenResponseSchema"
                }
              }
            }
          },
          "401": {
            "description": "The pairing code is unknown, already used, expired, or the AirTrack user who generated it no longer holds a premium plan. These are deliberately indistinguishable, so a guessed code cannot be confirmed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation: `code` must be exactly six digits and `name` between 1 and 100 characters. Malformed JSON fails here too.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. This endpoint draws no credits, so this is never credit exhaustion. The `Retry-After` header gives the number of seconds until the window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/airquality/score": {
      "post": {
        "tags": [
          "air-quality"
        ],
        "summary": "Score",
        "description": "A connection's air-quality poll: a location signal in, a score out.\n\nResolves the location (explicit lat/lon or a Wi-Fi scan), looks up air quality, and\nreturns a tiny ``{score, ttl}`` payload. ``401`` if the token is bad, ``403`` for a\nmissing scope or non-premium owner, ``422`` if no location can be resolved, ``429`` if\nrate-limited or over the credit quota (monthly pool or daily burst), ``503`` if\nupstreams are unavailable.",
        "operationId": "scoreAirQuality",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AQScoreRequestSchema"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AQScoreResponseSchema"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked connection token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The token does not carry the scope this endpoint requires, or the AirTrack user who owns the connection no longer holds a premium plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request body failed validation, or the Wi-Fi scan could not be resolved to a usable location. Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or the monthly credit pool or daily burst cap is exhausted. The `Retry-After` header gives the number of seconds until the relevant window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "A service this endpoint depends on is unavailable. The call is not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "ConnectionToken": []
          }
        ]
      }
    },
    "/v1/airquality/forecast": {
      "post": {
        "tags": [
          "air-quality"
        ],
        "summary": "Forecast",
        "description": "A connection's air-quality forecast: hourly Clean Air Score + pollutant\nconcentrations for a date at a location.\n\nReturns a shared ``timestamps`` axis plus ``measures`` (a map of measure to a\nparallel value list). If ``latitude``/``longitude`` are omitted, an approximate\nlocation is resolved from the caller's IP country. The window spans 4 days.\n``401`` if the token is bad, ``403`` for a missing scope or non-premium owner,\n``429`` if rate-limited or over the credit quota (monthly pool or daily burst),\n``400`` for an invalid date.",
        "operationId": "forecastAirQuality",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForecastRequestSchema"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForecastResponse"
                }
              }
            }
          },
          "400": {
            "description": "The requested date is outside the supported forecast window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked connection token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The token does not carry the scope this endpoint requires, or the AirTrack user who owns the connection no longer holds a premium plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or the monthly credit pool or daily burst cap is exhausted. The `Retry-After` header gives the number of seconds until the relevant window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "ConnectionToken": []
          }
        ]
      }
    },
    "/v1/airquality/routes": {
      "post": {
        "tags": [
          "air-quality"
        ],
        "summary": "Enrich Route",
        "description": "Enrich an ordered route of waypoints with air quality: per-waypoint ``measures``\nplus a route-level ``summary`` (banded clean-air-score, relative inhaled dose,\naverage pollutants).\n\nProvide a ``time`` on every waypoint (a recorded track), or on none of them with a\nrequest ``start_time`` (durations are then estimated from distance and activity speed).\n``400`` if the times fall outside the window air-quality data exists for, ``401`` bad\ntoken, ``403`` missing scope or non-premium owner, ``429`` if rate-limited or over the\ncredit quota (monthly pool or daily burst), ``503`` if no air-quality data is\navailable for the route.",
        "operationId": "enrichRoute",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrichRouteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrichedRoute"
                }
              }
            }
          },
          "400": {
            "description": "A waypoint time (or `start_time`) asks for air quality outside the window data exists for \u2014 more than 30 days in the past, or beyond the 84-hour forecast horizon. Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked connection token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The token does not carry the scope this endpoint requires, or the AirTrack user who owns the connection no longer holds a premium plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or the monthly credit pool or daily burst cap is exhausted. The `Retry-After` header gives the number of seconds until the relevant window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "No air-quality data could be produced for this route. Not charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "ConnectionToken": []
          }
        ]
      }
    },
    "/v1/routes": {
      "post": {
        "tags": [
          "route-planning"
        ],
        "summary": "Submit",
        "description": "Submit an async A-B route plan (premium-only).\n\nReturns a ``request_id``; poll ``GET /v1/routes/{request_id}`` for the result.\n``401`` bad token, ``403`` missing scope, non-premium owner or a partner-owned\nconnection, ``429`` if rate-limited or over the credit quota, ``400`` if the route\nexceeds the distance cap.",
        "operationId": "submitRoutePlan",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanRouteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRouteSubmitResponseSchema"
                }
              }
            }
          },
          "400": {
            "description": "The route is longer than the 15 km planning limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked connection token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The token does not carry the `routes:plan` scope, the AirTrack user who owns the connection no longer holds a premium plan, or the connection is partner-owned \u2014 planning requires a user-owned connection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The request failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or the monthly credit pool or daily burst cap is exhausted. The `Retry-After` header gives the number of seconds until the relevant window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "ConnectionToken": []
          }
        ]
      }
    },
    "/v1/routes/{request_id}": {
      "get": {
        "tags": [
          "route-planning"
        ],
        "summary": "Result",
        "description": "Poll for a route plan result. Always ``200`` with a lifecycle ``status``;\n``404`` if the id is unknown or belongs to a different AirTrack user. Free, and\ndeliberately not premium-gated: a lapsed user can still collect a result they paid\nfor.",
        "operationId": "getRoutePlan",
        "security": [
          {
            "ConnectionToken": []
          }
        ],
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Request Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanPollResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked connection token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The token does not carry the `routes:plan` scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No plan with that id, or it belongs to a different AirTrack user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The `request_id` in the path is not a valid UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. This endpoint draws no credits, so this is never credit exhaustion. The `Retry-After` header gives the number of seconds until the window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/quota": {
      "get": {
        "tags": [
          "quota"
        ],
        "summary": "Quota",
        "description": "The caller's shared API credit pool \u2014 the one balance every credit-drawing ``/v1``\nendpoint spends from \u2014 for the current month, plus the daily burst cap.\n\nAny valid connection token may read its own quota; no particular scope is required.",
        "operationId": "getQuota",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotaResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown or revoked connection token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. This endpoint draws no credits, so this is never credit exhaustion. The `Retry-After` header gives the number of seconds until the window resets.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the exhausted window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "ConnectionToken": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AQHISchema": {
        "properties": {
          "score": {
            "type": "number",
            "title": "Score"
          },
          "band": {
            "type": "string",
            "title": "Band"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "colour": {
            "type": "string",
            "title": "Colour"
          }
        },
        "type": "object",
        "required": [
          "score",
          "band",
          "description",
          "colour"
        ],
        "title": "BandedScore",
        "description": "A value on a banded scale: the number plus its band, label and colour.\n\nTitled ``BandedScore`` in generated contracts because it bands more than AQHIs \u2014\nthe /v1 route summary uses it for the Clean Air Score."
      },
      "AQScoreRequestSchema": {
        "properties": {
          "wifi": {
            "items": {
              "$ref": "#/components/schemas/WifiAccessPointSchema"
            },
            "type": "array",
            "title": "Wifi",
            "description": "Wi-Fi scan to resolve server-side, strongest first."
          },
          "lat": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 90.0,
                "minimum": -90.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Lat",
            "description": "WGS-84 latitude."
          },
          "lon": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 180.0,
                "minimum": -180.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Lon",
            "description": "WGS-84 longitude."
          },
          "client": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^[a-z0-9_-]{1,32}$"
              },
              {
                "type": "null"
              }
            ],
            "title": "Client",
            "description": "Optional caller self-identification for usage attribution (e.g. `synthetic_probe`). Recognised labels are attributed as `client:<name>`; unrecognised ones fall back to the endpoint's own label. Never affects the answer."
          }
        },
        "type": "object",
        "title": "AQScoreRequestSchema",
        "description": "A request for the air-quality score at a location.\n\nThe location may be given directly as ``lat``/``lon`` (e.g. a device with GNSS, or\nfor testing) or as a ``wifi`` access-point scan to be resolved server-side. At least\none must be provided; when both are present, ``lat``/``lon`` win and no geolocation\nlookup is made.",
        "examples": [
          {
            "lat": 51.5074,
            "lon": -0.1278
          },
          {
            "wifi": [
              {
                "mac": "a4:5e:60:c1:9b:2f",
                "rssi": -52
              }
            ]
          }
        ]
      },
      "AQScoreResponseSchema": {
        "properties": {
          "score": {
            "type": "integer",
            "maximum": 100.0,
            "minimum": 0.0,
            "title": "Score",
            "description": "Clean Air Score: 100 = cleanest, 0 = worst."
          },
          "ttl": {
            "type": "integer",
            "title": "Ttl",
            "description": "Seconds until the client should poll again.",
            "examples": [
              3600
            ]
          }
        },
        "type": "object",
        "required": [
          "score",
          "ttl"
        ],
        "title": "AQScoreResponseSchema",
        "description": "The tiny payload a connection polls for.\n\n``score`` is the Clean Air Score, 100 (cleanest) \u2026 0 (worst); ``ttl`` is the seconds\nuntil the client should poll again (server-controlled cadence). Any banding/labelling\nof the score is a client-side concern.",
        "examples": [
          {
            "score": 82,
            "ttl": 3600
          }
        ]
      },
      "ActivityType": {
        "type": "string",
        "enum": [
          "archery",
          "at_rest",
          "badminton",
          "baseball",
          "basketball",
          "bowling",
          "boxing",
          "bus",
          "car",
          "climbing",
          "core_training",
          "cricket",
          "curling",
          "cycling",
          "cycling_electric",
          "cycling_hand",
          "cycling_stationary",
          "dance",
          "disc_sports",
          "elliptical",
          "equestrian_sports",
          "fencing",
          "fishing",
          "fitness_gaming",
          "football_american",
          "football_australian",
          "frisbee",
          "golf",
          "gymnastics",
          "handball",
          "hiking",
          "hockey_field",
          "hockey_ice",
          "kickboxing",
          "lacrosse",
          "martial_arts",
          "metro",
          "mind_and_body",
          "motorbike",
          "other",
          "paddle_sports",
          "padel",
          "paragliding",
          "pickleball",
          "pilates",
          "preparation_and_recovery",
          "racquetball",
          "rowing_indoor",
          "rowing_outdoor",
          "rugby",
          "running_outdoor",
          "running_treadmill",
          "sailing",
          "skating_sports",
          "skipping",
          "snow_sports",
          "soccer",
          "softball",
          "squash",
          "stair_climbing",
          "strength_training",
          "stretching",
          "surfing_sports",
          "swimming_indoor",
          "swimming_outdoor",
          "table_tennis",
          "tennis",
          "track_and_field",
          "train",
          "triatholon",
          "underwater_diving",
          "unknown",
          "volleyball",
          "walking",
          "water_fitness",
          "water_polo",
          "water_sports",
          "wheelchair_run_pace",
          "wheelchair_walk_pace",
          "workout",
          "wrestling",
          "yoga"
        ],
        "title": "ActivityType"
      },
      "ConnectionTokenResponseSchema": {
        "properties": {
          "connection_id": {
            "type": "string",
            "format": "uuid",
            "title": "Connection Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Scopes",
            "description": "The capabilities the user granted this connection."
          },
          "token": {
            "type": "string",
            "title": "Token",
            "description": "The bearer token \u2014 shown once, store it securely."
          }
        },
        "type": "object",
        "required": [
          "connection_id",
          "name",
          "scopes",
          "token"
        ],
        "title": "ConnectionTokenResponseSchema",
        "description": "Returned once, at pairing \u2014 the raw token is never retrievable again.",
        "examples": [
          {
            "connection_id": "9b2f1c3d-4e5a-4b6c-8d7e-0f1a2b3c4d5e",
            "name": "Living-room display",
            "scopes": [
              "aq:read"
            ],
            "token": "aapk_pdn_9f8e7d6c5b4a39281706"
          }
        ]
      },
      "EnrichRouteRequest": {
        "properties": {
          "waypoints": {
            "items": {
              "$ref": "#/components/schemas/WaypointInput"
            },
            "type": "array",
            "maxItems": 100,
            "minItems": 1,
            "title": "Waypoints"
          },
          "start_time": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Start Time",
            "description": "Required when the waypoints carry no timestamps; used with the activity's estimated speed to derive per-waypoint times."
          },
          "activity_type": {
            "$ref": "#/components/schemas/ActivityType",
            "default": "other"
          },
          "environment_type": {
            "$ref": "#/components/schemas/EnvironmentType",
            "default": "outdoor"
          }
        },
        "type": "object",
        "required": [
          "waypoints"
        ],
        "title": "EnrichRouteRequest",
        "description": "Enrich an ordered route of waypoints with air quality.",
        "examples": [
          {
            "activity_type": "walking",
            "environment_type": "outdoor",
            "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"
              }
            ]
          }
        ]
      },
      "EnrichedRoute": {
        "properties": {
          "waypoints": {
            "items": {
              "prefixItems": [
                {
                  "type": "number"
                },
                {
                  "type": "number"
                }
              ],
              "type": "array",
              "maxItems": 2,
              "minItems": 2
            },
            "type": "array",
            "title": "Waypoints",
            "description": "[lat, lon] per waypoint."
          },
          "measures": {
            "additionalProperties": {
              "items": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "type": "array"
            },
            "type": "object",
            "title": "Measures",
            "description": "Map of measure name to a list of values parallel to `waypoints` (element *i* describes `waypoints[i]` at its point in time). Keys are `clean_air_score` (0-100, higher is cleaner, adjusted for the request's activity and environment) and pollutant concentrations in \u00b5g/m\u00b3 \u2014 typically `pm25`, `pm10`, `no2` and `o3`. Only measures with data for this route appear; `null` entries mark waypoints a measure could not be computed for.",
            "default": {}
          },
          "summary": {
            "$ref": "#/components/schemas/RouteSummary"
          }
        },
        "type": "object",
        "required": [
          "waypoints",
          "summary"
        ],
        "title": "EnrichedRoute",
        "description": "An enriched route: the input waypoints, per-waypoint ``measures`` (columnar, parallel\nto the waypoints), and a route-level ``summary``.",
        "examples": [
          {
            "measures": {
              "clean_air_score": [
                82.0,
                78.0
              ],
              "pm25": [
                8.1,
                9.6
              ]
            },
            "summary": {
              "average_pollutants": {
                "pm25": 8.85
              },
              "clean_air_score": {
                "band": "5",
                "colour": "#00C853",
                "description": "Excellent",
                "score": 80.0
              },
              "inhaled_dose": 38.4
            },
            "waypoints": [
              [
                51.5074,
                -0.1278
              ],
              [
                51.5121,
                -0.1236
              ]
            ]
          }
        ]
      },
      "EnvironmentType": {
        "type": "string",
        "enum": [
          "indoor",
          "outdoor",
          "unknown"
        ],
        "title": "EnvironmentType"
      },
      "ErrorResponse": {
        "properties": {
          "detail": {
            "type": "string",
            "title": "Detail",
            "description": "A human-readable description of what went wrong."
          }
        },
        "type": "object",
        "required": [
          "detail"
        ],
        "title": "ErrorResponse",
        "description": "The body of every non-2xx ``/v1`` response.",
        "examples": [
          {
            "detail": "Token lacks required scope: aq:read"
          }
        ]
      },
      "ForecastRequestSchema": {
        "properties": {
          "latitude": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 90.0,
                "minimum": -90.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Latitude",
            "description": "WGS-84 latitude. Omit both coordinates to use the caller's IP location."
          },
          "longitude": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 180.0,
                "minimum": -180.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Longitude",
            "description": "WGS-84 longitude."
          },
          "date": {
            "type": "string",
            "format": "date",
            "title": "Date"
          },
          "timezone": {
            "type": "string",
            "title": "Timezone",
            "description": "IANA timezone name for the date.",
            "examples": [
              "Europe/London"
            ]
          },
          "activity_type": {
            "$ref": "#/components/schemas/ActivityType",
            "default": "other"
          },
          "environment_type": {
            "$ref": "#/components/schemas/EnvironmentType",
            "default": "outdoor"
          }
        },
        "type": "object",
        "required": [
          "date",
          "timezone"
        ],
        "title": "ForecastRequestSchema",
        "description": "Request schema for fetching hourly Clean Air Scores for a date at a location.",
        "examples": [
          {
            "date": "2026-07-08",
            "latitude": 51.5074,
            "longitude": -0.1278,
            "timezone": "Europe/London"
          }
        ]
      },
      "ForecastResponse": {
        "properties": {
          "location": {
            "type": "string",
            "title": "Location",
            "description": "Human-readable name of the forecast location."
          },
          "timestamps": {
            "anyOf": [
              {
                "items": {
                  "type": "number"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Timestamps",
            "description": "Shared time axis, epoch seconds (parallel to each measure)."
          },
          "measures": {
            "additionalProperties": {
              "items": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "type": "array"
            },
            "type": "object",
            "title": "Measures",
            "description": "Map of measure name to a list of hourly values parallel to `timestamps` (element *i* is the value at `timestamps[i]`). Keys are `clean_air_score` (0-100, higher is cleaner) and pollutant concentrations in \u00b5g/m\u00b3 \u2014 typically `pm25`, `pm10`, `no2`, `o3` and `so2`. Only measures with data for this location appear; `null` entries mark hours a measure has no value for.",
            "default": {}
          }
        },
        "type": "object",
        "required": [
          "location"
        ],
        "title": "ForecastResponse",
        "description": "Hourly air quality for a date at a location.\n\nA single shared time axis (``timestamps``, epoch seconds) plus ``measures`` \u2014 a\nmap of measure name to a parallel list of values. ``measures`` carries the Clean\nAir Score (``clean_air_score``, 0\u2013100, higher is better) alongside pollutant\nconcentrations in \u00b5g/m\u00b3 (``pm25``, ``pm10``, ``no2``, ``o3``, ``so2``).",
        "examples": [
          {
            "location": "London, UK",
            "measures": {
              "clean_air_score": [
                82.0,
                79.0
              ],
              "pm25": [
                8.1,
                9.4
              ]
            },
            "timestamps": [
              1783497600,
              1783501200
            ]
          }
        ]
      },
      "HealthIndexType": {
        "type": "string",
        "enum": [
          "canada_aqhi",
          "hongkong_aqhi",
          "aal_aqhi",
          "clean_air_score"
        ],
        "title": "HealthIndexType"
      },
      "LocationInput": {
        "properties": {
          "latitude": {
            "type": "number",
            "maximum": 90.0,
            "minimum": -90.0,
            "title": "Latitude"
          },
          "longitude": {
            "type": "number",
            "maximum": 180.0,
            "minimum": -180.0,
            "title": "Longitude"
          }
        },
        "type": "object",
        "required": [
          "latitude",
          "longitude"
        ],
        "title": "LocationInput",
        "description": "A single geographic point for a planning request."
      },
      "PlanActivityType": {
        "type": "string",
        "enum": [
          "walking",
          "cycling",
          "running"
        ],
        "title": "PlanActivityType",
        "description": "Activity types supported by A-B route planning."
      },
      "PlanPollResponse": {
        "properties": {
          "status": {
            "$ref": "#/components/schemas/PlanRouteStatus"
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlanResult"
              },
              {
                "type": "null"
              }
            ]
          },
          "error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Error"
          }
        },
        "type": "object",
        "required": [
          "status"
        ],
        "title": "PlanPollResponse",
        "description": "Poll response for ``GET /v1/routes/{request_id}``.\n\n``status`` is always present. ``result`` is populated once ``status`` is\n``completed``; ``error`` is populated when ``status`` is ``failed``.",
        "examples": [
          {
            "status": "pending"
          },
          {
            "result": {
              "activity_type": "walking",
              "best_route_index": 0,
              "original_clean_air_score": 75.5,
              "original_distance": 1530.0,
              "original_dose": 36.0,
              "routes": [
                {
                  "clean_air_score_delta_pct": 9.2,
                  "distance": 1650.0,
                  "distance_delta": 120,
                  "dose_delta_pct": 14.0,
                  "duration": 1180.0,
                  "goal": {
                    "description": "Lowest pollution exposure",
                    "title": "Cleanest"
                  },
                  "measures": {
                    "clean_air_score": [
                      84.0,
                      81.0
                    ]
                  },
                  "summary": {
                    "clean_air_score": 82.5,
                    "inhaled_dose": 31.0
                  },
                  "waypoints": [
                    [
                      51.5007,
                      -0.1246
                    ],
                    [
                      51.5194,
                      -0.127
                    ]
                  ]
                }
              ]
            },
            "status": "completed"
          }
        ]
      },
      "PlanResult": {
        "properties": {
          "activity_type": {
            "$ref": "#/components/schemas/PlanActivityType"
          },
          "routes": {
            "items": {
              "$ref": "#/components/schemas/PlannedRoute"
            },
            "type": "array",
            "title": "Routes"
          },
          "original_distance": {
            "type": "number",
            "title": "Original Distance",
            "description": "Baseline route distance in metres."
          },
          "original_clean_air_score": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Original Clean Air Score"
          },
          "original_dose": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Original Dose"
          },
          "best_route_index": {
            "type": "integer",
            "title": "Best Route Index",
            "default": 0
          }
        },
        "type": "object",
        "required": [
          "activity_type",
          "routes",
          "original_distance"
        ],
        "title": "PlanResult",
        "description": "A completed plan: the optimised routes plus the baseline they are compared to.\n\nCleaner alternatives come first. ``best_route_index`` points at the route with the\nlowest inhaled dose (falling back to the highest Clean Air Score). ``original_*``\ndescribe the baseline direct route; ``original_clean_air_score``/``original_dose``\nare ``null`` when no comparable baseline was available."
      },
      "PlanRouteRequest": {
        "properties": {
          "start": {
            "$ref": "#/components/schemas/LocationInput"
          },
          "end": {
            "$ref": "#/components/schemas/LocationInput"
          },
          "activity_type": {
            "$ref": "#/components/schemas/PlanActivityType"
          },
          "start_time": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Start Time",
            "description": "Optional UTC start time for the route. Defaults to now."
          }
        },
        "type": "object",
        "required": [
          "start",
          "end",
          "activity_type"
        ],
        "title": "PlanRouteRequest",
        "description": "Request to plan air-quality-optimised routes between two points.\n\nThe direct distance between ``start`` and ``end`` must be no more than 15 km; longer\nrequests are rejected with ``400``. The limit was previously referenced in the docs\nbut never stated anywhere a caller could find it (#1377).",
        "examples": [
          {
            "activity_type": "walking",
            "end": {
              "latitude": 51.5194,
              "longitude": -0.127
            },
            "start": {
              "latitude": 51.5007,
              "longitude": -0.1246
            }
          }
        ]
      },
      "PlanRouteStatus": {
        "type": "string",
        "enum": [
          "pending",
          "completed",
          "failed"
        ],
        "title": "PlanRouteStatus"
      },
      "PlanRouteSubmitResponseSchema": {
        "properties": {
          "request_id": {
            "type": "string",
            "format": "uuid",
            "title": "Request Id"
          }
        },
        "type": "object",
        "required": [
          "request_id"
        ],
        "title": "PlanRouteSubmitResponseSchema",
        "description": "Acknowledgement that a route plan has been accepted for processing.\n\nPlanning is asynchronous: the work runs on a worker, and the client polls with the\nreturned ``request_id`` until the status is ``completed`` or ``failed``. Polling is\nfree \u2014 it draws no credits \u2014 and is rate-limited to 60 requests per minute.",
        "examples": [
          {
            "request_id": "b1e5b0a2-3c4d-4e6f-8a90-1c2d3e4f5a6b"
          }
        ]
      },
      "PlannedRoute": {
        "properties": {
          "waypoints": {
            "items": {
              "prefixItems": [
                {
                  "type": "number"
                },
                {
                  "type": "number"
                }
              ],
              "type": "array",
              "maxItems": 2,
              "minItems": 2
            },
            "type": "array",
            "title": "Waypoints",
            "description": "[lat, lon] per waypoint."
          },
          "measures": {
            "additionalProperties": {
              "items": {
                "anyOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "type": "array"
            },
            "type": "object",
            "title": "Measures",
            "description": "Map of measure name to a list of values parallel to `waypoints` (element *i* describes `waypoints[i]` at its point in time). Keys are `clean_air_score` (0-100, higher is cleaner, adjusted for the request's activity and environment) and pollutant concentrations in \u00b5g/m\u00b3 \u2014 typically `pm25`, `pm10`, `no2` and `o3`. Only measures with data for this route appear; `null` entries mark waypoints a measure could not be computed for.",
            "default": {}
          },
          "summary": {
            "$ref": "#/components/schemas/RouteSummary"
          },
          "distance": {
            "type": "number",
            "title": "Distance",
            "description": "Total route distance in metres."
          },
          "distance_delta": {
            "type": "integer",
            "title": "Distance Delta",
            "description": "Distance difference from the baseline route in metres (positive = longer)."
          },
          "clean_air_score_delta_pct": {
            "type": "number",
            "title": "Clean Air Score Delta Pct",
            "description": "Percentage improvement in Clean Air Score over the baseline route."
          },
          "dose_delta_pct": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Dose Delta Pct",
            "description": "Percentage less pollution inhaled vs the baseline route (positive = cleaner); null when there is no comparable baseline."
          },
          "duration": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Duration",
            "description": "Estimated duration in seconds."
          },
          "goal": {
            "$ref": "#/components/schemas/RouteGoal"
          }
        },
        "type": "object",
        "required": [
          "waypoints",
          "summary",
          "distance",
          "distance_delta",
          "clean_air_score_delta_pct",
          "goal"
        ],
        "title": "PlannedRoute",
        "description": "An optimised alternative route: the shared enriched-route core (waypoints,\nper-waypoint ``measures``, route-level ``summary``) plus planning-only extras\ncomparing it against the baseline (direct) route.",
        "examples": [
          {
            "clean_air_score_delta_pct": 9.2,
            "distance": 1650.0,
            "distance_delta": 120,
            "dose_delta_pct": 14.0,
            "duration": 1180.0,
            "goal": {
              "description": "Lowest pollution exposure",
              "title": "Cleanest"
            },
            "measures": {
              "clean_air_score": [
                84.0,
                81.0
              ],
              "pm25": [
                7.4,
                8.2
              ]
            },
            "summary": {
              "average_pollutants": {
                "pm25": 7.8
              },
              "clean_air_score": {
                "band": "5",
                "colour": "#00C853",
                "description": "Excellent",
                "score": 82.5
              },
              "inhaled_dose": 31.0
            },
            "waypoints": [
              [
                51.5007,
                -0.1246
              ],
              [
                51.5194,
                -0.127
              ]
            ]
          }
        ]
      },
      "Pollutant": {
        "type": "string",
        "enum": [
          "pm25",
          "no2",
          "o3",
          "so2",
          "pm10",
          "pm1",
          "co2",
          "tvoc",
          "tvoc_index"
        ],
        "title": "Pollutant"
      },
      "QuotaResponse": {
        "properties": {
          "used": {
            "type": "integer",
            "title": "Used"
          },
          "limit": {
            "type": "integer",
            "title": "Limit"
          },
          "remaining": {
            "type": "integer",
            "title": "Remaining"
          },
          "daily_used": {
            "type": "integer",
            "title": "Daily Used"
          },
          "daily_limit": {
            "type": "integer",
            "title": "Daily Limit"
          },
          "daily_remaining": {
            "type": "integer",
            "title": "Daily Remaining"
          }
        },
        "type": "object",
        "required": [
          "used",
          "limit",
          "remaining",
          "daily_used",
          "daily_limit",
          "daily_remaining"
        ],
        "title": "QuotaResponse",
        "description": "The caller's shared API credit pool \u2014 the one balance every credit-drawing ``/v1``\nendpoint spends from. Redeeming a pairing code, reading this quota and polling for a\nroute-plan result are free.\n\n``used``/``limit``/``remaining`` are the monthly pool (the primary cost guard);\n``daily_*`` are the daily burst cap. Counted in credits (\u2248 upstream AQ calls).",
        "examples": [
          {
            "daily_limit": 100,
            "daily_remaining": 60,
            "daily_used": 40,
            "limit": 1000,
            "remaining": 872,
            "used": 128
          }
        ]
      },
      "RedeemPairingCodeSchema": {
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 6,
            "minLength": 6,
            "pattern": "^\\d{6}$",
            "title": "Code",
            "description": "The 6-digit code the user generated in the app."
          },
          "name": {
            "type": "string",
            "maxLength": 100,
            "minLength": 1,
            "title": "Name",
            "description": "A label to identify this connection."
          }
        },
        "type": "object",
        "required": [
          "code",
          "name"
        ],
        "title": "RedeemPairingCodeSchema",
        "description": "Sent by a connecting client (unauthenticated) to redeem a code for a token.\n\nThe 6-digit ``code`` is the proof of ownership \u2014 it was just typed in by the user\nfrom the app. ``name`` labels the connection (e.g. \"Pom Pom\").",
        "examples": [
          {
            "code": "048213",
            "name": "Living-room display"
          }
        ]
      },
      "RouteGoal": {
        "properties": {
          "title": {
            "type": "string",
            "title": "Title"
          },
          "description": {
            "type": "string",
            "title": "Description"
          }
        },
        "type": "object",
        "required": [
          "title",
          "description"
        ],
        "title": "RouteGoal",
        "description": "Why this alternative was surfaced \u2014 a human-readable goal label."
      },
      "RouteSummary": {
        "properties": {
          "clean_air_score": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/AQHISchema"
              },
              {
                "type": "null"
              }
            ],
            "description": "Time-weighted Clean Air Score for the route (0-100, higher is cleaner) with its band number, label and colour."
          },
          "inhaled_dose": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Inhaled Dose",
            "description": "Relative exposure dose integrated over the route's duration \u2014 higher means more inhaled risk. Unitless; comparable across routes and with route planning's `dose`."
          },
          "average_pollutants": {
            "additionalProperties": {
              "type": "number"
            },
            "propertyNames": {
              "$ref": "#/components/schemas/Pollutant"
            },
            "type": "object",
            "title": "Average Pollutants",
            "description": "Mean concentration per pollutant, \u00b5g/m\u00b3."
          }
        },
        "type": "object",
        "title": "RouteSummary",
        "description": "Route-level air-quality summary.\n\nOne health metric only \u2014 the Clean Air Score, banded with the same vocabulary the\nAirTrack app shows (Excellent \u2026 Very Poor). The AQHI the score derives from stays\ninternal: publishing both invited misreading, since the two run in opposite\ndirections (score: higher is cleaner; AQHI: higher is worse)."
      },
      "WaypointInput": {
        "properties": {
          "lat": {
            "type": "number",
            "maximum": 90.0,
            "minimum": -90.0,
            "title": "Lat",
            "description": "WGS-84 latitude."
          },
          "lon": {
            "type": "number",
            "maximum": 180.0,
            "minimum": -180.0,
            "title": "Lon",
            "description": "WGS-84 longitude."
          },
          "time": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Time",
            "description": "Timestamp at this waypoint (ISO-8601); see the mode note above."
          }
        },
        "type": "object",
        "required": [
          "lat",
          "lon"
        ],
        "title": "WaypointInput",
        "description": "A point on the route. ``time`` is optional: provide it on every waypoint to enrich\na recorded track, or omit it on all and set the request ``start_time`` to have durations\nestimated from distance and activity speed."
      },
      "WifiAccessPointSchema": {
        "properties": {
          "mac": {
            "type": "string",
            "title": "Mac",
            "description": "The access point's BSSID.",
            "examples": [
              "a4:5e:60:c1:9b:2f"
            ]
          },
          "rssi": {
            "type": "integer",
            "title": "Rssi",
            "description": "Signal strength in dBm (negative).",
            "examples": [
              -52
            ]
          }
        },
        "type": "object",
        "required": [
          "mac",
          "rssi"
        ],
        "title": "WifiAccessPointSchema",
        "description": "A single Wi-Fi access point from a scan, used for geolocation.\n\n``mac`` is the BSSID; ``rssi`` is the signal strength in dBm (negative). Send the\nstrongest few APs, strongest first. BSSIDs are location-revealing personal data \u2014 we\nresolve them to a coarse point and never log the raw values."
      }
    },
    "securitySchemes": {
      "ConnectionToken": {
        "type": "http",
        "description": "A connection token issued at pairing, sent as `Authorization: Bearer aapk_\u2026`. Tokens are environment-specific: the segment after `aapk_` (`pdn`, `tst`) names the issuing environment.",
        "scheme": "bearer",
        "bearerFormat": "aapk"
      }
    }
  },
  "tags": [
    {
      "name": "air-quality",
      "description": "Score a location, forecast a day's air quality, or enrich a route you supply. Responses speak the AirTrack Clean Air Score (100 = cleanest, 0 = worst) and \u2014 for routes \u2014 the AirTrack Air Quality Health Index (AAL AQHI) and inhaled dose, alongside raw pollutant concentrations in \u00b5g/m\u00b3."
    },
    {
      "name": "route-planning",
      "description": "Plan lower-exposure walking, running and cycling routes between two points. Planning is asynchronous: submit a request, then poll for the optimised alternatives, each carrying per-waypoint measures and an exposure summary."
    },
    {
      "name": "connections",
      "description": "Exchange a pairing code for a connection token. The AirTrack user generates a short-lived 6-digit code in the app and enters it into your client; your client redeems it here for a long-lived `aapk_` bearer token carrying exactly the capabilities the user granted. This is the only unauthenticated endpoint. Redeeming a code draws no credits. Managing a connection afterwards \u2014 listing, renaming, revoking \u2014 is done by its owner in the AirTrack app, not through this API."
    },
    {
      "name": "quota",
      "description": "Check the shared API credit pool \u2014 the single balance every credit-drawing endpoint spends from. Any valid connection token can read its own monthly pool and daily burst usage; the read itself is free."
    }
  ]
}