This document is intended mainly for application developers that want to use air quality data in their projects. We recommend reading this documentation, especially sections explaining some basic concepts. We hope this documentation will be helpful in the process of integrating with our API. We make every effort to keep this documentation up to date and complete. If you have any questions please don't hesitate do contact us. This documentation is open-source and can be edited on GitHub.

What's new in API 2.0

This documentation applies to API version 2.0. Comparing to previous version we are happy to announce lots of improvements:

  • "Sensors" have been replaced in the API with "Installations".
  • We added support for various types of indexes and air quality standards.
  • We added simpler and more intuitive API Endpoints. We have simplified and unified response models. We added better request validation and error handling. We have removed some unsupported / not working query parameters and redundant or non-intuitive endpoints.
  • The API returns now more details about installation "Sponsor".
  • The API returns now better installation address data and the altitude at which the sensor is installed.
  • The API returns now color representing pollution level and additional text describing particular air quality index.
  • The API returns now measurement units and supported types of measurements.
  • The API returns now texts translated to multiple languages.

API 1.0 support

Since 28.02.2019 the support for API 1.0 has been dropped.

General information

Airly API is based on REST. Our API has predictable and intuitive URLs, organized around resources (Installations and Measurements). We use built-in HTTP features for issuing requests (HTTP methods) and handling errors (HTTP statuses).

It is possible to interact with our API from a client-side web applications served from a different domain - our API supports cross-origin requests.

All the API endpoints return content in JSON format (including errors) along with Content-Type: application/json HTTP header encoded using charset UTF-8, unless noted otherwise in the endpoint description.


Using our API requires registering an account. It is also possible to log in with existing GitHub, Google or Facebook account.

A registered user is provided with an apikey which should be passed on each API request for authentication. The API key can be passed in a request as a custom header named apikey or as a query parameter.


To authenticate, Airly API expects you to pass an apikey header with your API key passed in as a value.


apikey: <apikey>

Example Request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

Terms of use and limits

Terms of use

On a free plan, API consumer is required to use our API only in non-commercial projects.

When presenting data obtained from our API you should present our logo: svg, png.

More details are available in our Terms of Service.


In order to maintain high API throughput, availability and quality of the service the API access is rate-limited.

Default rate limits per apikey are 1000 API requests per day and 50 API requests per minute for all users.

All the HTTP requests are counted towards the limit, regardless of whether the request succeeded or not. Each request decrements the currently available limit by 1. The counters are reset each day at midnight UTC (the daily limit) and every full minute (the minute limit).

tip The default limits allow you e.g. to query measurements of particular sensor every 1.5 minute for the whole day or you can query measurements of 40 sensors every hour for the whole day. This should be sufficient for most of the individual use cases.

In case of applications displaying data continuously we recommend caching API responses in local storage and refreshing them periodically.

In case of exceeding the limit API request will return HTTP 429 - Too Many Requests.

If you need an increased limit or want to use our data in a commercial project please contact us.

RateLimit Headers

Each API response contains additional HTTP headers that inform about current API key limits and their usage. Header names begin with X-RateLimit-Limit- and X-RateLimit-Remaining-.


X-RateLimit-Limit-day: 1000 X-RateLimit-Limit-minute: 50 X-RateLimit-Remaining-day: 996 X-RateLimit-Remaining-minute: 46


Some API endpoints return textual content, e.g. description of current air quality. Such texts are translated and returned in a language according to user preference.

To select API content language you should include an Accept-Language HTTP header in your request with value set to desired language.

Currently supported languages are English ("en" - default) and Polish ("pl").


Correctly issued API requests result in 200 OK HTTP status and a JSON content. Otherwise an HTTP error code status is returned, depending on situation. Table below explains it in detail:

400 - Bad Request request was incorrect, e.g. query parameters were invalid
401 - Unauthorized no API key was provided
404 - Not Found request path or parameters point to a non-existent resource
406 - Method Not Allowed request attempts to use invalid HTTP method (e.g. POST instead of GET)
406 - Not Acceptable request attempts to use unsupported content type (e.g. Accept text/xml instead of application/json)
429 - Too Many Requests API rate limit was exceeded (see Limits)
500 - Internal Server Error a server error has occured

Additionally in case of an error a JSON response is returned in following format:


{ "errorCode": <error code>, "message": <message describing error>, "details": { <key-value map with more details describing the error> } }

Example request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

Error response


HTTP Status: 400


{ "errorCode": "API_REQUEST_INVALID", "message": "API Request was not valid", "details": { "violations": [ { "parameter": "lat", "message": "latitude value must be between -90.0 and +90.0", "rejectedValue": 200 } ] } }


All coordinates that occur in the API (both as query parameters and as returned payload fields) are according to WGS 84 standard.

Coordinates are a pair of numbers named latitude (or lat) and longitude (lng) and represent decimal degrees i.e. degrees without minutes or seconds, but instead as floating point numbers with fractional part representing fraction of a degree.

latitude (lat) values can range from -90.0 to +90.0, and longitude (lng) values can range from -180.0 to +180.0.


All the time values that occur in the API (both as query parameters and as returned payload fields) are ISO8601 timestamps according to UTC timezone e.g. 2018-08-24T08:24:48.652Z.

Average measurements that are calculated for particular period of time (e.g. hourly averages) are returned along with that time period. This period is given as a pair of attributes fromDateTime and tillDateTime and represents a left-closed and right-open time interval [fromDateTime, tillDatetime).

GZIP compression

All the API endpoints support GZIP compression. Enabling compression significantly reduces API response size and can speed up its transfer, and it is a recommended practice.

In order to enable compression an API request should contain additional HTTP header Accept-Encoding: gzip. Keep in mind that the HTTP client used for communication should be properly configured to handle compressed responses.

Example request


curl -X GET -sD - \ --compressed \ --header 'Accept: application/json' \ --header 'Accept-Encoding: gzip' \ --header 'apikey: <apikey>' \ ''



HTTP/2 200 date: Tue, 18 Sep 2018 13:18:19 GMT content-type: application/json;charset=UTF-8 ... content-encoding: gzip { ... }


Wherever possible, this API will be maintained in a backwards compatible manner.

JSON responses have following stability guarantees:

  • Properties of JSON objects will continue to have same name and value type
  • Properties marked as mandatory will always be returned and will never be removed from the API
  • Properties marked as optional may or may not be returned by the API and as such may be removed from the API permanently
  • Dynamic structures like arrays and maps may or may not contain some elements; in particular they may be empty sometimes
  • New properties may be added to the responses at any time, but they will not collide with existing properties, nor alter their meaning

tip In practice these rules mean that for properties marked as mandatory you can always expect them and there is no need to check for their presence. Optional properties must always be checked and null or missing values handled properly. Arrays and maps must not be expected to contain specific values, or concrete number of elements.

E.g. an installation that for some reason only measures weather conditions (it may have PM sensor broken) will not return the PM values in its values array.

When using JSON parsing libraries it is important to configure them in a way to ignore unknown fields, as we may be extending the API and adding new properties to responses without notice.

If it is necessary to change some operations, or its response models in a way that is not backwards compatible, a new endpoint will be created with new response model. We will then maintain both old and new endpoint for an extended period of time, until the old one is safe to be deprecated and dropped. We will inform you if this ever has to occur.

The behaviour of the API may change without warning if the existing behaviour is incorrect or constitutes a security vulnerability.



In the previous API version we were dealing with a concept of a sensor. You could e.g. query what sensors are present in a given area, and then query each individual sensor for its measurements by the sensorId. Unfortunately such approach can lead to some problems. Our devices fleet is constantly growing and it is inevitable that at some point particular device will have to be replaced or moved to another location. When that happens, querying for data by sensorId can lead to incorrect results, since you would be querying for a device that was in fact removed, or replaced.

In the new API version we solve this problem by introducing a concept of installation. An installation is an entity that binds together a sensor and its location where it's installed at a particular time. Thus you can always query an installation safely without risking you would be querying incorrect sensor.

In case a sensor is removed from its location, future queries for its installation can result in either:

  • if the sensor was replaced with another one, queries will result in HTTP 301 Moved Permanently with Location header pointing to a new url you should query. In addition, a JSON payload is returned:


{ "errorCode": "INSTALLATION_REPLACED", "message": "Installation was replaced with another one", "details": { "successorId": <new installation identifier> } }
  • if there is no replacement sensor in the same location (no "successorId") then queries will result in 404 Not Found

In principle we recommend using other API endpoints, e.g. those that return measurements by location, or those that return average measurements for a wider area. Then you don't heave to bother with installations, IDs, etc. However, if you still need to query particular installation, just keep in mind the above rules and handle returned codes properly.


An installation sponsor is some sort of organization or institution that purchased our sensor. Usually that would be local authorities (e.g. city or county), but also private companies and individual customers. We have also given away many devices during some campaigns, such as #PolskaOddycha or #KrakówOddycha.

Information about who is the sponsor of particular installation are available on our platform, both in the API and on our map. By presenting particular brand or city on our platform, we kind of want to say "thank you" to our customers for joining the fight for clean air. The API provides: name, logo (url to image file) and a link to sponsor's website.

When presenting our data in a public place (e.g. your application or website) there is no requirement to show who is the sponsor of that installation, although it can't be presented in a way that is vague or misleading. E.g. if you are just displaying an average from multiple installations for the entire city, you should state that clearly.


All the API endpoints that begin with a prefix /v2/measurements return unified response format that contains measurements from particular installation or area (depending on query parameters). Each response contains fields:

  • current - these are measurements data for the last hour (moving average over last 60 minutes)
  • history - these are historical measurements, currently last 24 full hours that contain average hourly measurements, in ascending order
  • forecast - these are measurements forecasts, currently next 24 full hours that contain average anticipated measurements, in ascending order

Each of those fields contains both raw measurement values, as well as indexes calculated using those measurements, and air quality standards violations (where applicable).

When presenting our data in your application it is recommended to indicate clearly what particular data fields mean to avoid any confusion.

Measurement types

Our devices are capable of measuring various types of measurements: temperature, humidity, atmospheric pressure, particulate matter concentrations etc. Our API also gives access to data from other measurement stations (e.g. GIOŚ stations) which measure only some of these parameters (e.g. only PM10). To provide access to all this data in a unified and standard way, measurement data is returned in a dynamic structure in the form of a list (JSON array). The list elements are "name-value" pairs, each for a particular type of measurement.

A list of all types of measurements supported by the API, their names and units used can be retrieved by querying /v2/meta/measurements.


Air quality index is a way to convert raw measurements data for various pollutants (e.g. dust, gases) into single value representing amount of air pollution. Air quality index usually defines a number from an arbitrary scale (e.g. 0-100), some quantized pollution level (e.g. "Low", "High"), a color representing particular pollution level (e.g. green, red) and sometimes additional text or description e.g. a warning about potential health effects of the pollution.

The goal of air quality index is to present air pollution data in a simple way which is easy to understand by majority of people, especially those unfamiliar with air quality standards and e.g. don't know whether the particular PM10 concentration in the air is safe or not. Secondly, the air quality index reduces data for various pollutants (e.g. dusts, gases) to one value and represents a unified (and somewhat simplified) picture of air quality in a given place, regardless of which air pollutant dominates in a given area.

Air quality indexes are defined by national and international organizations responsible for environmental protection. There exist various indexes in different countries. For example, in the European Union, the CAQI index prepared as part of the CITEAIR project is widely used. In Poland, the Polish Air Quality Index (PIJP) is also defined and calculated by the Chief Inspectorate for Environmental Protection (GIOŚ). Because different indexes have different calculation methods, the same measurement data in a given area can be interpreted as different index values, different pollution levels and be represented by different colors.

The Airly platform currently supports two air quality indexes: CAQI and Polish Air Quality Index - PIJP. The API responses contain the index calculated for the measurement data specified by the indexType query parameter.

The list of all indexes supported by the platform and the definitions of their levels and ranges with colors can be obtained by querying the address /v2/meta/indexes.


The highest level of the CAQI index starts at 100 index value and it doesn't show further changes in pollution in intuitive way. Unfortunately, in Poland during the winter season, the level of dust concentrations often exceeds and remains above 100 CAQI on large areas, sometimes around the clock. To better illustrate this problem, we decided to introduce additional CAQI levels with assigned color values. This is how the AirlyCAQI index was created. The AirlyCAQI numeric value is calculated exactly the same as for CAQI, it differs only in the definition of levels for the highest pollution concentrations. It also has more levels (7, while CAQI defines only 5) and colors that are a little nicer, chosen especially for our platform. The AirlyCAQI index is the default index returned by the API.


The air quality standard, also sometimes referred to as a "norm" or a "guideline", is a specific concentration of a concrete component of air pollution, which is considered as an acceptable limit. Pollutants exceeding this value may have negative impact on our health, and the more they exceed the limit the greater the impact may be. Air quality standards are defined by the national and international health organizations, e.g. the World Health Organization (WHO).

In the Airly API, we provide the standard values of PM10 and PM2.5. The API response contains the standard (limit) value and the pollutant concentration expressed as a percentage of the limit.

API endpoints


Operations in group /v2/installations/ allow to search and list available installations and retrieve their metadata, e.g. location, address data etc. All the operations return unified payload response with following attributes:

  • id (integer) - unique installation identifier; having installationId you can query measurements from particular installation
  • latitude, longitude (double) - installation coordinates; exact sensor location on map
  • address:
    • country, city, street, number (string) - installation address data; each attribute is optional, may be empty or null
    • displayAddress1, displayAddress2 (string) - extra fields describing address, e.g. school name or number; optional fields
  • elevation (double) - altitude at which device is installed; meters above mean sea level (mamsl)
  • airly (bool) - flag indicating whether this installation is an Airly device or not; e.g. GIOŚ stations are flagged as false
  • sponsor:
    • id (integer) - sponsor identifier
    • name (string) - name of this installation's sponsor
    • description (string) - additional description of this installation's sponsor; optional field
    • logo (string) - url pointing to sponsor's logo (jpg image); optional field
    • link (string) - url pointing to sponsor's website; optional field

Example Installation


{ "id": 204, "location": { "latitude": 50.062006, "longitude": 19.940984 }, "address": { "country": "Poland", "city": "Kraków", "street": "Mikołajska", "number": "4B", "displayAddress1": "Kraków", "displayAddress2": "Mikołajska" }, "elevation": 220.38, "airly": true, "sponsor": { "name": "KrakówOddycha", "description": "Airly Sensor is part of action", "logo": "ówOddycha.jpg", "link": "" } }

GET /v2/installations/{installationId}

Endpoint returns single installation metadata, given by installationId.

Required parameters;

  • installationId (integer) - url path parameter; installation identifier

Example request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

GET /v2/installations/nearest

Endpoint returns list of installations which are closest to a given point, sorted by distance to that point.

Required parameters:

  • lat (double) - latitude as decimal degree, e.g. 50.062006
  • lng (double) - longitude as decimal degree, e.g. 19.940984

Optional parameters:

  • maxDistanceKM (double) - default value 3.0; all the returned installations must be located within this limit from the given point (in km); negative value means no limit
  • maxResults (integer) - default value 1; maximum number of installations to return; negative value means no limit

Example request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''


Operations in group /v2/measurements allow to query measurements data by various criteria. All the operations return unified payload format, which contains following fields:

  • current - single element of type AveragedValues; contains averaged measurements for the last 60 minutes (moving average); for installations other than Airly (e.g. GIOŚ) could represent data up to 3 hours old, due to delays in third party data arrival
  • history - list of elements of type AveragedValues; contains a list of historical hourly averaged measurements for the last 24 full hours
  • forecast - list of elements of type AveragedValues; contains a list of anticipated future hourly averaged measurements for the next 24 full hours

tip The field history contains data for the last 24 full hours, e.g. if current time is 09:27 UTC, then that field will contain 24 elements, starting with hour interval 09:00-10:00 UTC yesterday, and ending with hour interval 08:00-09:00 UTC today.

The field forecast contains forecast data for the next 24 full hours, where the first hour is the current hour; e.g. if current time is 09:27 UTC, then that field will contain 24 elements, starting with hour interval 09:00-10:00 UTC today, and ending with hour interval 08:00-09:00 UTC tomorrow.

It is worth noting that the history and forecast fields form an uninterrupted sequence of data for 48 hours, of which the first 24 hours are measured values, and the next 24 hours are forecast values.

The field current contains averaged measurements for the last 60 minutes (moving average), up until "now", i.e. the time of the request. For example, if the current time is 09:27 UTC, this field contains data for the time interval 08:27-09:27 UTC. However, for installations other than Airly (e.g. GIOŚ stations) this field may contain data up to 3 hours old, due to delays in third party data arrival.

AveragedValues contains:

  • fromDateTime / tillDateTime (string) - ISO8601 timestamp; date and time in UTC; the two fields represent time interval during which the data in this payload was measured and averaged
  • values - array of objects of type Value; single element contains:
    • name (string) - name (type) of the given measurement; e.g. PM10
    • value (double) - measured value of the given measurement type; e.g. concentration 25µg/m3
  • indexes - array of objects of type Index; single element represents an index value calculated for the measurements in this payload
    • name (string) - name of the index (e.g. CAQI, or PIJP)
    • value (double) - numerical value of the calculated index
    • level (string) - discreet level of the index (e.g. HIGH, or LOW etc.)
    • description (string) - description of the index level; the text translated and returned in a language according to Accept-Language request header
    • advice (string) - additional text with advice regarding current index level; the text translated and returned in a language according to Accept-Language request header
    • color (string) - color representing index level; in a form of hexadecimal RGB triplet (e.g. #D1CF1E)
  • standards - array of objects of type Standard; single element represents particular air quality standard
    • name (string) - name of this standard; currently only WHO standards are supported
    • pollutant (string) - name of the pollutant to which this standard applies, currently only PM10 and PM25 are supported
    • limit (double) - certain average concentration level of the pollutant which should not be exceeded over given period of time; in other words, this is the value for which the pollutant concentration reaches 100% of the limit
    • percent (double) - concentration value of a given pollutant expressed as a percentage of this concentration in the standard / limit; e.g. for a standard of 50μg/m3 of PM10 the concentration of 25μg/m3 is expressed as 50%


Query parameters common for all endpoints:

  • indexType - selects the type of the index which should be calculated and returned in response; currently supported values are AIRLY_CAQI (default), CAQI, and PIJP; (in current API version only 1 selected index is returned)

Example Measurements


{ "current": { "fromDateTime": "2018-08-24T08:24:48.652Z", "tillDateTime": "2018-08-24T09:24:48.652Z", "values": [ { "name": "PM1", "value": 12.73 }, { "name": "PM25", "value": 18.7 }, { "name": "PM10", "value": 35.53 }, { "name": "PRESSURE", "value": 1012.62 }, { "name": "HUMIDITY", "value": 66.44 }, { "name": "TEMPERATURE", "value": 24.71 }, ... ], "indexes": [ { "name": "AIRLY_CAQI", "value": 35.53, "level": "LOW", "description": "Dobre powietrze.", "advice": "Możesz bez obaw wyjść na zewnątrz.", "color": "#D1CF1E" } ], "standards": [ { "name": "WHO", "pollutant": "PM25", "limit": 25, "percent": 74.81 }, ... ] }, "history": [ ... ], "forecast": [ ... ] }

GET /v2/measurements/installation

Endpoint returns measurements for concrete installation given by installationId.

In case of the installation does not exist, or it was already deinstalled, returns 404 Not Found.

In case of the installation was replaced with another device, returns 301 Moved Permanently with Location header pointing to new URL of this installation's replacement measurements.

Required parameters:

  • installationId (integer) - installation identifier

Example request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

GET /v2/measurements/nearest

Endpoint returns measurements for an installation closest to a given location.

If no installation could be found within maxDistanceKM from the given point, then returns 404 Not Found.

Required parameters:

  • lat (double) - latitude as decimal degree, e.g. 50.062006
  • lng (double) - longitude as decimal degree, e.g. 19.940984

Optional parameters:

  • maxDistanceKM (double) - default value 3.0; the searched installation must be located within this limit from the given point (in km); negative value means no limit

Example request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

GET /v2/measurements/point

Endpoint returns measurements for any geographical location.

Measurement values are interpolated by averaging measurements from nearby sensors (up to 1,5km away from the given point). The returned value is a weighted average, with the weight inversely proportional to the distance from the sensor to the given point.

Required parameters:

  • lat (double) - latitude as decimal degree, e.g. 50.062006
  • lng (double) - longitude as decimal degree, e.g. 19.940984

Example request


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''


Some data types in the API have dynamic nature and their values and ranges can change over time. E.g. in the future our API may support new measurement types (e.g. gases) or new index types used in different countries. Operations in group /v2/meta return information about current types and values supported by the API.

tip With meta operations, you can build more dynamic applications and user interfaces that adapt on the fly to the data returned by the API. For example, on our map a colorful legend describing index levels is created thanks to the meta operation.

GET /v2/meta/indexes

Endpoint returns a list of all the index types supported in the API along with lists of levels defined per each index type.

The returned array contains objects of type IndexType, which contain following fields:

  • name (string) - name of this index type, e.g. AIRLY_CAQI
  • levels - if of index levels definitions; contains objects of type IndexLevel:
    • values (string) - range of index values that this level represents, e.g. 0-25
    • level (string) - name of this index level e.g. "VERY_LOW"
    • description (string) - description of this index level; e.g. "Very Low"; this is a translated field, the value is returned in a language according to Accept-Language request header
    • color (string) - color representation of this index level, given by hexadecimal RGB triplet (e.g. #6BC926)

Example request:


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

Example response


[ { "name": "AIRLY_CAQI", "levels": [ { "values": "0-25", "level": "VERY_LOW", "description": "Very Low", "color": "#6BC926" }, ... ] }, ... ]

GET /v2/meta/measurements

Endpoint returns list of all the measurement types supported in the API along with their names and units.

The returned array contains objects of type MeasurementType which contain following fields:

  • name (string) - name of this measurement type, e.g. PM10
  • label (string) - descriptive name of this measurement type (field translated)
  • unit (string) - unit of this measurement type e.g. µg/m3

Example request:


curl -X GET \ --header 'Accept: application/json' \ --header 'apikey: <apikey>' \ ''

Example response


[ { "name": "PM10", "label": "PM10", "unit": "µg/m³" }, { "name": "TEMPERATURE", "label": "Temperature", "unit": "°C" }, ... ]


We put a lot of effort to make this documentation very clear and understandable. If you feel like there is something we can improve, you can suggest your changes via GitHub. Thanks!