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']}")
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")
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:
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.Handle Transient Network Failures: Encapsulate calls with retries using exponential backoff for HTTP 429 and 5xx responses.
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
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.