Getting Started with the Python PurpleAir API Wrapper: Sensor Discovery, Keys & Rate Limits

Note

  • Published: November 04, 2026

  • Authors: Written by Gemini 3.8 Flash, Co-Authored & Reviewed by Carlos Santos (@carlkidcrypto)

  • Estimated Reading Time: 5 minutes

  • Tags: PurpleAir • Python • API • IoT • Air-Quality • Sensors • Open-Source

Environmental air quality monitoring has evolved from centralized municipal stations to dense hyper-local sensor networks. PurpleAir has established one of the largest citizen-science sensor ecosystems globally, measuring airborne particulate matter (PM1.0, PM2.5, PM10) alongside ambient temperature, relative humidity, and atmospheric pressure.

Extracting programmatic telemetry from PurpleAir’s REST API historically required writing manual HTTP routines, tracking read versus write credentials, managing query parameter encoding, and parsing JSON responses. The official Python wrapper, purpleair_api (published on PyPI as purpleair-api), streamlines these interactions into a clean, modular Python 3 interface.

In this tutorial, we will explore the core architecture of purpleair_api, examine credential separation, inspect sensor discovery patterns, and implement resilient polling workflows that safely handle API rate limits.

Understanding the PurpleAir API Architecture

The PurpleAir API separates access permissions between reading public or private sensor telemetry and modifying cloud-hosted resources. To interact with the service, developers must obtain dedicated API keys from PurpleAir:

  • Read Key (X-API-Key): Required for querying individual sensors, batching multi-sensor field requests, executing bounding-box geographic queries, and retrieving group data.

  • Write Key (X-API-Key): Required for mutating actions, such as creating custom sensor groups, registering member sensors to a group, and managing cloud metadata.

  • Local Access: Modern PurpleAir sensors running on your local network expose a LAN JSON endpoint that does not require cloud API keys.

The purpleair_api package mirrors this domain structure through specialized submodules coordinated by a unified facade class, PurpleAirAPI:

  • PurpleAirAPI: The primary convenience entry point. It accepts optional read keys, write keys, and local IP addresses, automatically initializing the corresponding functional clients.

  • PurpleAirReadAPI: Dedicated client for read-only queries against the central PurpleAir cloud infrastructure.

  • PurpleAirWriteAPI: Dedicated client for creating and modifying sensor groups and member associations.

  • PurpleAirLocalAPI: Direct LAN client for querying local sensors over HTTP without consuming cloud API credits.

Installation and Requirements

The package requires Python 3.11 or newer (officially supported on Python 3.11 through 3.15, declaring python_requires = >=3.11,<3.16) and relies on the foundational requests library for reliable HTTP networking.

Install the package directly from PyPI:

pip install purpleair-api

Alternatively, you can install the latest development branch directly from GitHub:

git clone https://github.com/carlkidcrypto/purpleair_api.git
cd purpleair_api/python
pip install .

Initializing the Client

Depending on your application’s security posture and scope, you can initialize either the unified PurpleAirAPI facade or a focused submodule.

Using the Unified Facade

When building data pipelines that both read telemetry and organize sensors into tracked groups, instantiate PurpleAirAPI with your read and write keys:

import os
from purpleair_api.PurpleAirAPI import PurpleAirAPI

# Retrieve credentials from environment variables
read_key = os.getenv("PURPLEAIR_READ_KEY")
write_key = os.getenv("PURPLEAIR_WRITE_KEY")

# Initialize the unified wrapper
paa = PurpleAirAPI(
    your_api_read_key=read_key,
    your_api_write_key=write_key
)

# Query sensor data directly through the facade
sensor_id = 282168
sensor_data = paa.request_sensor_data(sensor_index=sensor_id)
print(f"Sensor Name: {sensor_data['sensor']['name']}")

📥 Download this script: 01_using_the_unified_facade.py

Using the Focused Read Client

For distributed worker agents, analytical dashboards, or microservices that strictly consume telemetry without management privileges, instantiate PurpleAirReadAPI. This avoids exposing write credentials to read-only environments:

import os
from purpleair_api.PurpleAirReadAPI import PurpleAirReadAPI

read_key = os.getenv("PURPLEAIR_READ_KEY")
read_client = PurpleAirReadAPI(api_read_key=read_key)

# Request specific environmental telemetry fields
fields = [
    "name",
    "pm2.5_atm",
    "pm2.5_cf_1",
    "temperature",
    "humidity",
    "pressure",
]
telemetry = read_client.request_sensor_data(
    sensor_index=282168,
    fields=",".join(fields)
)

sensor = telemetry["sensor"]
print(f"Station: {sensor.get('name')}")
print(f"PM2.5 (ATM): {sensor.get('pm2.5_atm')} ug/m3")
print(f"Temperature: {sensor.get('temperature')} deg F")

📥 Download this script: 02_using_the_focused_read_client.py

Querying Multiple Sensors and Bounding Boxes

Querying individual sensors sequentially across a regional footprint introduces unnecessary network latency and exhausts API points quickly. The PurpleAir API supports querying multiple sensors in a single transaction via geographic bounding boxes or custom sensor index lists.

With PurpleAirReadAPI, you can specify rectangular geographic coordinates defined by northwest and southeast latitude/longitude corners:

import os
from purpleair_api.PurpleAirReadAPI import PurpleAirReadAPI

read_client = PurpleAirReadAPI(os.getenv("PURPLEAIR_READ_KEY"))

# Bounding box coordinates for the Inland Northwest (Palouse region)
nwlng = -117.20
nwlat = 46.80
selng = -116.90
selat = 46.65

# Request specific fields to minimize payload overhead
query_fields = "name,latitude,longitude,pm2.5_atm,temperature"

response = read_client.request_multiple_sensors_data(
    fields=query_fields,
    nwlng=nwlng,
    nwlat=nwlat,
    selng=selng,
    selat=selat,
    location_type=0  # 0 = Outside sensors, 1 = Inside sensors
)

fields_header = response.get("fields", [])
data_rows = response.get("data", [])
print(f"Discovered {len(data_rows)} sensors in bounding box.")

# Map fields to values for easy dictionary access
for row in data_rows[:5]:
    row_dict = dict(zip(fields_header, row))
    print(
        f"Sensor {row_dict.get('name')}: "
        f"PM2.5 = {row_dict.get('pm2.5_atm')} ug/m3"
    )

📥 Download this script: 03_querying_multiple_sensors_and_bounding_b.py

Managing Sensor Groups with Write Credentials

When tracking a designated fleet of outdoor air quality monitors (such as municipal parks, school districts, or industrial perimeters), querying by bounding box can capture irrelevant or unverified devices. Creating a custom sensor group organizes your target fleet under a single, reusable entity.

Using PurpleAirWriteAPI, you can programmatically provision groups and register sensor members:

import os
from purpleair_api.PurpleAirWriteAPI import PurpleAirWriteAPI

write_key = os.getenv("PURPLEAIR_WRITE_KEY")
write_client = PurpleAirWriteAPI(api_write_key=write_key)

# Create a new sensor tracking group
new_group = write_client.post_create_group_data(
    name="Municipal Palouse Monitors"
)
group_id = new_group["group_id"]
print(f"Created Group ID: {group_id}")

# Register target sensors into the newly created group
target_sensors = [282168, 175420, 194382]
for sensor_id in target_sensors:
    write_client.post_create_member(
        group_id=group_id,
        sensor_index=sensor_id
    )
    print(f"Enrolled sensor {sensor_id} into group {group_id}")

📥 Download this script: 04_managing_sensor_groups_with_write_creden.py

Once populated, the entire group can be queried using request_members_data on your read client, retrieving batch readings for enrolled sensors in a single API transaction.

Rate Limiting and Production Resilience

The PurpleAir API enforces rate limits and credit-based points accounting. High-frequency polling without rate control risks receiving HTTP 429 (Too Many Requests) or 402 (Payment Required) responses.

To ensure production stability, implement exponential backoff, request throttling, and field filtering:

  1. Request Only Required Fields: The PurpleAir API measures point costs partially based on returned fields. Requesting * or omitted field filters drains your account balance significantly faster. Always specify the exact comma-separated fields required by your telemetry pipeline.

  2. Handle Transient Network Failures: Encapsulate calls with retries using exponential backoff for HTTP 429 and 5xx responses.

  3. Prefer Local API for LAN Sensors: If your sensors reside on the same local network as your logging service, use PurpleAirLocalAPI. Local polling queries the device directly over LAN, eliminating cloud API credit costs and cloud internet dependencies.

Here is a resilient query helper pattern:

import time
from purpleair_api.PurpleAirAPI import PurpleAirAPIError
from requests.exceptions import RequestException

def resilient_sensor_query(client, sensor_index, fields, max_retries=3):
    fields_str = ",".join(fields)
    backoff_seconds = 2.0

    for attempt in range(1, max_retries + 1):
        try:
            return client.request_sensor_data(
                sensor_index=sensor_index,
                fields=fields_str
            )
        except (RequestException, PurpleAirAPIError) as exc:
            msg = str(exc)
            if any(code in msg for code in ("400", "401", "403")):
                raise
            if attempt == max_retries:
                err_msg = (
                    f"Sensor {sensor_index} failed after {attempt} tries"
                )
                raise RuntimeError(err_msg) from exc
            time.sleep(backoff_seconds)
            backoff_seconds *= 2.0

📥 Download this script: 05_resilient_sensor_query.py

Summary & What’s Next

The purpleair_api package provides a robust, Pythonic abstraction over the PurpleAir REST API. By separating read, write, and local responsibilities, it enables developers to build clean data ingestion pipelines without managing raw HTTP sessions or manual query string construction.

In our next blog post, we will explore purpleair_data_logger, examining how to automate continuous ingestion of PurpleAir sensor streams into TimeScaleDB and PostgreSQL databases for long-term time-series analysis and Grafana dashboards.


Warning

AI-generated content may contain misinformation or mistakes. A human does review this, but we are only human.