Making API Calls with the PurpleAir API

This guide will help you understand the basics of sending HTTP requests and making API calls. This article assumes you to have a basic knowledge of programming.


All sensor data reported from PurpleAir sensors are able to be collected from our database using the PurpleAir API. The API follows REST principles and uses the methods GET, POST, PUT, and DELETE. PurpleAir’s API documentation can be viewed at

The PurpleAir API uses two different API keys; a read key and a write key, which need to be used to make specific API calls. If you want to use the PurpleAir API yourself, you can create your own API keys here: PurpleAir Develop. API keys are issued per user, not per sensor.

You will need a Google associated email to sign in. You can learn how to associate a different email address here: Sign in to your Google Account with another email address - Computer - Google Account Help.

The data collected with the API will include the Raw PM values and not the AQI. Since there are so many different ways to display air quality data, any conversion of the raw data to AQI values will need to be performed.

Make Your First API Call

There are three parts to making an API request; the request line, header, and body.

Request Line: The request line will first include one of the four REST methods mentioned previously; GET, POST, PUT, and DELETE. A GET request is used to collect data entries. A POST request is used to create data entries. A PUT request is used to update existing data entries. A DELETE request is used to remove existing data entries.

A URL will follow the request method. The URL is the web address we want to send a request to. For the PurpleAir API, the URL will start with What comes after that will depend on the type of request we want to make.

Header: Next comes the header. The header holds the context for our request. When using the PurpleAir API, this will always include your respective API key for the specific call you want to make. The parameter for this is named “X-API-Key”.

Body: Last of all is the body. The body will include the payload that we want to send through the API. The only requests that will require a body are POST and PUT requests.

With all three of these parts put together we have a complete request. The last thing to do is to send it.

Let’s look at an example of a request to check what type of API Key we are sending. This example will be completed using Curl.

curl -X GET "" -H "X-API-Key: ********-****-****-****-************"

Here you can see some of the parts of a request that were mentioned earlier. This request uses GET as the request method and as the URL, and together they make up the request line, which is preceded by -X when using Curl.

The header of this request is "X-API-Key: ********-****-****-****-************", which is recognized in cURL by a string that follows a -H. One of your API keys would replace the line of asterisks. Either key would work here, but for this example we will use a read key.

Since there is no data that we will need to send to the URL, a body does not need to be included.

Then, inside of a terminal, we can put in our request and it will return something very similar to this:

  "api_version" : "V1.0.10-0.0.12",
  "time_stamp" : 1609459200,
  "api_key_type" : "READ"

This is the data that our request has collected. It returned three separate items; the current version of the API, the time this request was completed, and the type of API key we sent. As you can see, since we put in a read key, the “api_key_type” it returned was “READ”. If we had entered a write key, the request would have returned “WRITE”.

The Sensor Index

To access a specific device through the API, you will need that device’s unique sensor index. You can learn where to locate the sensor index and other information here.

Collect Sensor Data

Now that we’ve been able to successfully complete an API call, let’s try one slightly more complex. This next request will be used to collect the real-time data of any specific public sensor on the PurpleAir map.

curl -X GET "*****" -H "X-API-Key: ********-****-****-****-************"

This request is very similar to the last one that checked the API key, but notice that the URL in this request ends with /sensors instead of /keys. This part of the URL will change depending on what data we want to collect. After /sensors, you will also see a short line of asterisks. Here you will put the sensor index of the sensor you want to collect data from.

We will need to use a read API key here since we are going to be collecting a data entry.

If we now put this request inside of a terminal and send it, it will return something similar to this:

  "api_version" : "V1.0.10-0.0.12",
  "time_stamp" : 1609459200,
  "data_time_stamp" : 1609460400,
  "sensor" : {

You may notice the beginning of the return looks very similar to the return we got from our last request, but after that follows a “data_time_stamp” variable and a “sensor” variable that will include a lot of information that I have replaced here with an ellipsis ‘…’. More information on the data that is returned in this call can be seen in the PurpleAir API documentation under Sensors > Get Sensor Data > Sensor data fields.

Use of Groups

Create a New Group

If you are planning on querying data from multiple sensors at once, we ask that instead of sending an individual request for each sensor, you send a single request that queries all of them. You can request data from multiple sensors at once by using groups.

A group is an array of sensors under a specific name and ID that is linked to a set of API keys. When making a call to create a group you must assign it a name. Once it is created, it will be given its own ID. Then a user can populate that group with sensors, or members, as they are referred to in the documentation. Once a group is populated, it can be queried in a single request and that request will return the data of all of the members included within it. Groups are assigned to the API keys they are created with.

Here is an example Curl request to create a group named “My Group”:

curl -X POST "" -H "Content-Type: application/json" -H "X-API-Key: ********-****-****-****-************" -d '{"name":"My Group"}'

As we have seen before this request contains a request line and a header. However it also includes a parameter that starts with -d. This is the body and the ‘d’ stands for data. What follows is the payload that we are going to be sending in our request. As mentioned previously to create a group you must assign it a name. This is done by including the following JSON in the payload; '{"name":"My Group"}'. You can change the name of the group to anything you feel appropriate, but for this example we will leave it as “My Group”.

Once we put in and enter this command, it will return something similar to the following example:

  "api_version" : "V1.0.10-0.0.12",
  "time_stamp" : 1609459200,
  "group_id": 001

As usual, you can see the API version and the time stamp are included, but afterwards you are able to see a new variable. This is the group ID that is now assigned to the group you have just created. It is the ID you will use to query data from this new group.

If you ever need to see the IDs of the groups you have created, you can query those by using the Get Groups List request, and if you want to see the individual members within a specific group, you can do that using the Get Group Detail request.

Populate a Group With Members

Now that we have created a group we can populate it with members. The request we are going to use to do this is the Create Member request.

Here is another example Curl request that adds a member:

curl -X POST "***/members" -H "Content-Type: application/json" -H "X-API-Key: ********-****-****-****-************" -d '{"sensor_index":"*****"}'

In the URL of this request we will need to include the group ID of the group we want to add our sensor to and then follow that with /members. Since we are going to be adding a data entry we are going to input our API write key, and lastly we will input the corresponding sensor index for the sensor we want to add.

Once that is completed we can put it into a terminal and run it.

  "api_version" : "V1.0.10-0.0.12",
  "time_stamp" : 1609459200,
  "data_time_stamp" : 1609460400,
  "group_id" : 001,
  "member_id" : 001,
  "sensor" : {

This return will appear very similar to the output from the getSensorData request but also includes two new values. These values are the group_id and the member_id. The group_id is the ID if the group the sensor was added to and the member_id is a new ID that was created specifically for the sensor to be accessed within the set group.

Query Data From a Group

Now that we have a group with members, we can collect sensor data from that group as a whole. We can do this using the Get Member Data request and the Get Members Data request.

The following example will use the Get Member Data request:

curl -X GET "***/members/***" -H "X-API-Key: ********-****-****-****-************"

As you can see the URL in this request will require both the group ID and the member ID of the sensor that you want to query. Then in the header we will include our API read key. Running this command will give an identical return to the Create Member request we demonstrated previously. It will return the appropriate time stamps, group and member IDs, and the sensor data.

With the Get Members Data request you would be able to query any amount of specified fields from all of the members within a specific group. Here we see an example of an API call collecting the longitude and latitude values of the members within a group:

  "api_version" : "V1.0.10-0.0.12",
  "time_stamp" : 1609459200,
  "data_time_stamp" : 1609460400,
  "group_id" : 001,
  "max_age" : 604800,
  "firmware_default_version" : "6.01",
  "fields" : [
  "data" : [

Here we see all of the fields we input will be returned to us in a variable named data. The sensor index of each sensor queried will always appear first in each entry, followed by the specified fields we entered.

Collect Data From a Private Sensor

The requests we have sent so far will work only for public PurpleAir sensors. If you would like to collect data from a private sensor, we will have to access it a bit differently. To collect data from a private sensor using the Get Sensor Data request, you will need access to the link that is included in the confirmation email received after registering the private sensor. If you do not have access to that link, then the other way to access a private sensor would be to use the Create Member request to add the private sensor to a group using the Owner’s Email of the private sensor.

Get Sensor Data Request

This request will be very similar to the first Get Sensor Data request we completed earlier, except we will be adding an extra parameter to the URL. But first, we need to get the read_key of the sensor we want to collect data from. This key can be found in the View on the PurpleAir map link in the confirmation email that was received after registering the private sensor. Locate the section of the URL that says, key=. What follows is the read_key that we will need to access the private sensor’s data. You will need this key for any data that was collected while the sensor is private, even if the sensor is no longer private.

More information is available here: Sensor Index.

Now that we have the read_key of the sensor we want to access, let’s see what an example request will look like.

curl -X GET "*****?read_key=*****" -H "X-API-Key: ********-****-****-****-************"

This request looks almost identical to the previous getSensorData request, except that we’ve added ?read_key=***** in the URL. In place of the line of asterisks, input the read_key of the sensor that has been collected. If we run our request, we will collect data from a private sensor.

Create Member Request

To add a private sensor to a group, we will use the Create Member request again, but with different parameters.

Here is an example Curl request that does that:

curl -X POST***/members -H 'Content-Type: application/json' -H "X-API-Key: ********-****-****-****-************" -d '{"sensor_id":"**:**:**:**:**:**", "owner_email": ""}'

Similar to the earlier group requests, this request has all three parts; a request line , a header , and a body . However, you may note that this request has two headers included. We have added another header here to tell the server what type of data we are going to be sending, which in this context is JSON, hence the addition of -H ‘Content-Type: application/json’.

In the body of the request, you can see that we have included two different values; the sensor_id and the owner_email. The sensor-id is different from what we had input previously, which was the sensor_index. The ID we want to use now is the Device-ID (MAC address) of the sensor we want data from. The Device-ID is a unique code for every sensor that is printed on the sensor sticker and listed in the shipping confirmation email.

For the owner_email value, we will input the email that was used in the Owner’s Email field on the registration form.

Now that that information has been input, we can put in our request and send it.

  "api_version" : "V1.0.10-0.0.12",
  "time_stamp" : 1609459200,
  "data_time_stamp" : 1609460400,
  "group_id" : 001,
  "member_id" : 001,
  "sensor" : {

This return will appear the same at the last createMember request, and now the private sensor you have added can be accessed as normal through the group using the Get Member Data request or the Get Members Data request.


Hi! Apologies if this is not the right place to ask, but I requested an API key by emailing a few days ago. I haven’t heard back from anyone, and wanted to check if that’s indeed the right way to request a new API key.

Thank you!

Hi @Vivek, our contact email is I see your message shows Ensure that the email is spelled correctly, and we will be happy to get you a set of API keys!

My embarrassment knows no bounds, thank you for pointing that out! Sending a request to the correct email :slight_smile:

Using the v1/sensors API with latitude and longitude NW/SE corners, the returned data seems to fill up with sensors with locations that are set to null. None of the sensors that show up on the map in the target area appear in the search results. Is there a way to ignore these?


Results in:

  "data" : [
[12831,"OB/Point Loma",null,null],
[145194,"508 Hoover Street, Nelson, BC",null,null],
[19659,"Elmhurst, IL",null,null],
[36293,"LAC - Monitor 2",null,null],
[36299,"LAC - Monitor 4",null,null],
[36307,"LAC - Monitor 5",null,null],
[36445,"LAC - Monitor 3",null,null],
[69853,"OUTSIDE DC",null,null],
[79179,"S. Military Rd, Portland, OR",null,null],
[79323,"Lytton Springs",null,null],
[95087,"329 W 24th Pl",null,null],
[101093,"Crestone Charter School",null,null],
[111504,"1810 6th street Berkeley",null,null],
[111642,"Jay Road",null,null],
[112054,"archbishop mitty high school",null,null],
[115125,"airport gardens",null,null],
[121211,"Westwood hill",null,null],

When I run this query, I get an "ApiKeyMissingError’. ??

@Steve_Drevik, your API keys will need to be added to your request. You can do so by adding “api_key=****” and replacing the asterisks with your API read key.

I’d just like to verify my understanding about retrieving data using the API. My understanding is that, when I use a GET query, I’ll receive the latest data values for the sensors I’ve requested. Is it true that there is no way, using the API, to retrieve several of the latest records at once, for example, by providing a timestamp and retrieving all of the records since that time?

When using the real-time API, you will always get the most recent data from the sensors you query. Using the “modified since” parameter and the timestamp of your previous request, you can filter out sensors that haven’t uploaded data since then. This will help avoid duplicate entries.

With the historical API, you can enter a timestamp and get the data for the set period. However, at this time, you can only query one sensor at a time with the historical endpoints. To gain access to the historical API, you will need to make a request.

1 Like

Hi Ethan!

Can I use same procedure to get historical data of a particular sensor? I’m new to all these and just started learning all the ropes bit by bit.

Thank you.

Hi! Apologies if this is not the right place to ask, but I requested an API key by emailing a few days ago. I haven’t heard back from anyone, and wanted to check if that’s indeed the right way to request a new API key.

Thank you!

1 Like

Hi @LeeHyunA, I saw your email was sent to us less than a day ago. Please give us one business day to grant API keys to new users.

Just so you are aware, our contact email is, without the “r” after the word “purple.”

I am having some problems adding members to an existing group using the suggested terminal command above. The error description I get is

“description” : “Cannot find a sensor with the provided parameters.”

If I use the same parameters on Loading..., it works fine. Any suggestions appreciated, thanks.

Hi Ethan,

I want to get the historical data of sensors in Chicago. I did collect sensors data including location and sensor_index via Get Sensors Data API. However, when using Get Sensor History API by providing collected sensor_index it showed that all collected sensor_index might be invalid. I want to know if there’s any solution.

prams = {
‘fields’: {‘humidity,temperature,pressure,voc,ozone1,scattering_coefficient,deciviews,visual_range,’
‘start_timestamp’:start_timestamp, #‘1560837600’,
‘end_timestamp’:endtimestamp, #‘1560924000’,
‘average’:average #‘60’,

Hi Ethan,

I may not be interpreting the response correctly, but it doesn’t seem like the modified_since parameter is having an effect.

As an example, using the URL:,channel_flags,channel_flags_manual,channel_flags_auto,channel_state,confidence,confidence_auto,confidence_manual,date_created,firmware_version,hardware,last_modified,last_seen,latitude,location_type,longitude,memory,model,name,pa_latency,position_rating,private,rssi,uptime&show_only=148461&modified_since=1694716084, I receive the response:

  api_version: 'V1.0.11-0.0.49',
  time_stamp: 1694716190,
  data_time_stamp: 1694716154,
  modified_since: 1694716084,
  max_age: 604800,
  firmware_default_version: '7.02',
  fields: [
    'sensor_index',         'last_modified',
    'date_created',         'last_seen',
    'private',              'name',
    'location_type',        'model',
    'hardware',             'firmware_version',
    'rssi',                 'uptime',
    'pa_latency',           'memory',
    'position_rating',      'latitude',
    'longitude',            'altitude',
    'channel_state',        'channel_flags',
    'channel_flags_manual', 'channel_flags_auto',
    'confidence',           'confidence_auto',
  location_types: [ 'outside', 'inside' ],
  channel_states: [ 'No PM', 'PM-A', 'PM-B', 'PM-A+PM-B' ],
  channel_flags: [ 'Normal', 'A-Downgraded', 'B-Downgraded', 'A+B-Downgraded' ],
  data: [
      'CleanAIRE NC Newell',
      '3.0+OPENLOG+31037 MB+DS3231+BME280+BME68X+PMSX003-A+PMSX003-B',

This response does reflect the modified_since argument passed (1694716084), yet it still returns data for a sensor with a last_seen timestamp of 1694716045. I assume last_seen is used as a comparison, rather than last_modified, but the sensor should not be shown in either case, right?

1 Like