# EPA Air Quality & AQI Lookup (`automation-lab/epa-air-quality-aqi-lookup`) Actor

Download official AirNow daily files and export normalized EPA air-quality observations and AQI values by date, place, site, pollutant, country, or U.S. state.

- **URL**: https://apify.com/automation-lab/epa-air-quality-aqi-lookup.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.04 / 1,000 item extracteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## EPA Air Quality & AQI Lookup

Download official AirNow daily observation files and turn them into clean, typed records for EPA air quality AQI analysis.

Choose one date, several dates, or a range of up to 31 days.
Filter by place, monitoring site, pollutant, country, or U.S. state FIPS code.
Export pollutant concentration, AQI, health category, coordinates, reporting agency, stable IDs, and source provenance.

This Actor reads the public `daily_data_v2.dat` files produced by AirNow, the U.S. EPA's nationwide air-quality reporting program.
It needs no AirNow API key, browser, cookies, or proxy.

### What does EPA Air Quality & AQI Lookup do?

For every requested date, the Actor:

1. Builds the official dated AirNow file URL.
2. Downloads the pipe-delimited daily observation file.
3. Parses only records matching the documented 13-field format.
4. Converts AirNow's `-999` AQI sentinel to `null`.
5. Adds a human-readable EPA AQI category.
6. Applies all requested location, site, pollutant, and country filters.
7. Stops at your global `maxItems` limit.
8. Saves normalized records to the default Apify dataset.

The result is ready for JSON, CSV, Excel, XML, RSS, database, webhook, or API integrations supported by Apify.

### Who is this Actor for?

- Environmental analysts collecting repeatable daily observation snapshots.
- Operations teams using AQI context in outdoor-work or logistics dashboards.
- Health and safety teams preparing non-regulatory situational reports.
- Researchers comparing pollutants, sites, or dates.
- Data engineers loading official public observations into a warehouse.
- Journalists checking the source readings behind an air-quality story.
- Developers who need a keyless source of preliminary AirNow measurements.

### Why use this AirNow data exporter?

AirNow's dated files are useful but low-level.
They use pipe delimiters, compact identifiers, numeric category codes, and `-999` missing-value sentinels.

The Actor adds:

- ISO `YYYY-MM-DD` dates.
- Explicit numeric and nullable fields.
- Stable `recordId` values for comparisons.
- Pollutant names separated from averaging-window labels.
- Human-readable AQI health categories.
- Country and U.S. state FIPS fields derived from AQS identifiers.
- Exact source URLs and retrieval timestamps.
- Input validation and bounded retries.
- A dataset view designed for analysts.

### What air-quality data is extracted?

| Field | Meaning |
| --- | --- |
| `recordId` | Stable date + site + parameter key |
| `observationDate` | Local date represented by the source row |
| `siteId` | AirNow site identifier |
| `fullAqsId` | Full AQS identifier with country prefix |
| `countryCode` | Three-digit country prefix, such as `840` |
| `stateFips` | Two-digit U.S. state FIPS code when applicable |
| `siteName` | Monitoring-site name |
| `parameter` | AirNow parameter, such as `OZONE-8HR` |
| `pollutant` | Pollutant name, such as `OZONE` or `PM2.5` |
| `units` | `PPB`, `PPM`, or `UG/M3` as reported |
| `value` | Peak or average pollutant concentration |
| `averagingPeriodHours` | 1, 8, or 24-hour period |
| `dataSource` | Reporting agency |
| `aqi` | AQI value when AirNow calculates one |
| `aqiCategoryCode` | Numeric category from 0 through 5 |
| `aqiCategory` | Good through Hazardous |
| `latitude`, `longitude` | Monitoring-site coordinates |
| `sourceUrl` | Official dated file URL |
| `retrievedAt` | Retrieval timestamp |
| `preliminary` | Always `true` for AirNow observations |

### How to get started

1. Open the Actor in Apify Console.
2. Keep the prefilled date or enter up to 31 real dates in `YYYY-MM-DD` format.
3. Optionally set a date range instead of `dates`.
4. Add one or more filters.
5. Choose whether rows without AQI should be included.
6. Set `maxItems` for the largest dataset you want.
7. Click **Start**.
8. Open the **Air quality observations** dataset view.
9. Export or integrate the records you need.

A useful first input is:

```json
{
  "dates": ["2025-09-01"],
  "countryCodes": ["840"],
  "pollutants": ["PM2.5", "OZONE"],
  "onlyWithAqi": true,
  "maxItems": 25
}
```

### Input parameters

#### `dates`

An array of up to 31 specific dates.
Use `YYYY-MM-DD`.
Do not combine it with `startDate` or `endDate`.
When no date input is provided, the Actor requests today's UTC date.

#### `startDate` and `endDate`

An inclusive date range of at most 31 days.
Both values are required together.
Use this mode for a monitoring window or a backfill.

#### `places`

Case-insensitive text filters.
Each value is matched against site name, reporting agency, short site ID, and full AQS ID.
A record is accepted when any supplied place value matches.

Examples include `Sacramento`, `California`, or a known site identifier.

#### `siteIds`

Exact short AirNow IDs or full AQS IDs.
Use this field when a workflow already tracks a fixed monitor list.

#### `pollutants`

Accepts base pollutants or exact parameters.
Examples:

- `PM2.5`
- `PM10`
- `OZONE`
- `OZONE-8HR`
- `CO`
- `NO2`
- `SO2`

A base pollutant matches its available averaging windows.

#### `countryCodes`

Three-digit prefixes from the full AQS identifier.
For example, `840` selects U.S. sites and `124` selects Canadian sites.

#### `stateFips`

Two-digit state FIPS codes for U.S. records.
For example, use `06` for California or `36` for New York.
Leading zeroes are required.

#### `onlyWithAqi`

Defaults to `true`.
AirNow calculates AQI for eligible 8-hour and 24-hour observations.
Set this to `false` to retain concentration rows whose AQI and category are `null`.

#### `maxItems`

The maximum number of records saved across all dates.
Allowed range: 1 to 50,000.
The default is 1,000.

### Output example

A real normalized row has this shape:

```json
{
  "recordId": "2025-09-01:840060070008:PM2.5-24hr",
  "observationDate": "2025-09-01",
  "siteId": "060070008",
  "fullAqsId": "840060070008",
  "countryCode": "840",
  "stateFips": "06",
  "siteName": "Chico -  East",
  "parameter": "PM2.5-24hr",
  "pollutant": "PM2.5",
  "units": "UG/M3",
  "value": 5.2,
  "averagingPeriodHours": 24,
  "dataSource": "California Air Resources Board",
  "aqi": 29,
  "aqiCategoryCode": 0,
  "aqiCategory": "Good",
  "latitude": 39.76168,
  "longitude": -121.84047,
  "sourceUrl": "https://files.airnowtech.org/airnow/2025/20250901/daily_data_v2.dat",
  "retrievedAt": "2026-09-05T06:11:14.945Z",
  "preliminary": true
}
```

Values change as AirNow updates current and previous daily files.

### How much does it cost to export EPA air quality observations?

The Actor uses pay-per-event pricing:

- **$0.005** once when a run starts.
- **$0.001736 per observation** on the BRONZE plan.
- Lower per-observation rates apply on higher Apify tiers.
- A row excluded by your filters is not charged as an observation.

At BRONZE rates, 25 records cost about **$0.0484** including the start event.
A 100-record export costs about **$0.1786**.
A 1,000-record export costs about **$1.741**.

These examples use the current BRONZE price and do not include unrelated platform charges outside the Actor's PPE events.

### Multi-day monitoring workflow

Use a range and a location filter to build a small repeatable feed:

```json
{
  "startDate": "2025-08-30",
  "endDate": "2025-09-01",
  "places": ["Sacramento"],
  "pollutants": ["OZONE"],
  "onlyWithAqi": true,
  "maxItems": 100
}
```

Schedule the Actor daily or weekly in Apify Console.
Use `recordId` to join repeated exports or detect changed rows.
Remember that recent source files may be updated as additional observations arrive.

### Spreadsheet and data-pipeline integrations

Common patterns include:

- Export the dataset as CSV or Excel for a one-time analysis.
- Connect Google Sheets for a shared environmental snapshot.
- Send completed-run webhooks to an ETL service.
- Load JSON records into BigQuery, Snowflake, PostgreSQL, or object storage.
- Compare `recordId` and `aqi` values in an automation workflow.
- Trigger internal review when an AQI category crosses a chosen threshold.

This Actor exports observations; it does not send alerts by itself.
Use Apify schedules, webhooks, or your own downstream automation for notifications.

### Run through the Apify API

Replace `YOUR_TOKEN` with your Apify token.

#### cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~epa-air-quality-aqi-lookup/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"dates":["2025-09-01"],"countryCodes":["840"],"maxItems":25}'
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/epa-air-quality-aqi-lookup').call({
  dates: ['2025-09-01'],
  countryCodes: ['840'],
  pollutants: ['PM2.5', 'OZONE'],
  maxItems: 25,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient('YOUR_TOKEN')
run = client.actor('automation-lab/epa-air-quality-aqi-lookup').call(run_input={
    'dates': ['2025-09-01'],
    'countryCodes': ['840'],
    'pollutants': ['PM2.5', 'OZONE'],
    'maxItems': 25,
})
items = client.dataset(run['defaultDatasetId']).list_items().items
print(items)
```

### Use with MCP and AI assistants

Add the Actor to Claude Code through Apify MCP:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/epa-air-quality-aqi-lookup"
```

#### Claude Desktop

Add this server in Claude Desktop's MCP configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/epa-air-quality-aqi-lookup"
    }
  }
}
```

#### Cursor

Use the same JSON server entry in Cursor's MCP settings, then enable the `apify` server.

#### VS Code

Add the same HTTP URL to your VS Code MCP server configuration and start the server from the MCP panel.

Example prompts:

- “Export 25 U.S. PM2.5 and ozone AQI observations for 2025-09-01.”
- “Get California PM2.5 readings and AQI categories for 2025-09-01.”
- “Collect Sacramento ozone AQI records from 2025-08-30 through 2025-09-01.”

### Data quality and limitations

AirNow describes these observations as preliminary and subject to change.
They are reported for public AQI reporting and forecasting.
They are not fully verified regulatory data.

Do not use this Actor's output to formulate or support regulations, compliance findings, long-term trends, or other official government decisions.
Use EPA's Air Quality System for validated regulatory data.

Other limitations:

- A requested date may not have a file.
- Current and previous daily files can change as reporting becomes more complete.
- Not every concentration row has an AQI.
- `places` is text matching, not geocoding or radius search.
- State FIPS filtering applies only to U.S. AQS identifiers.
- The Actor downloads at most 31 daily files per run.
- The source controls station coverage and reporting frequency.

### Failure behavior and troubleshooting

The Actor retries transient HTTP failures up to three times.
A missing date is logged and skipped when another requested date is available.
The run fails when none of the requested files exists.

**Why did I get zero results?**

Your files were available, but no row matched every active filter.
Try removing a site, place, state, or pollutant filter.
Set `onlyWithAqi` to `false` when concentration-only rows are useful.

**Why are `aqi` and `aqiCategory` null?**

AirNow uses `-999` when an AQI is not calculated for that parameter/window.
The Actor normalizes that sentinel to `null`.

**Why does today's run fail or look sparse?**

The dated file may not yet exist or may still be filling.
Try the previous date and rerun later if your schedule requires the newest available day.

**Why does a place match a reporting agency?**

The `places` field intentionally searches site name, agency, and IDs.
Use `siteIds`, `countryCodes`, or `stateFips` for exact scope.

### Legality and responsible use

AirNow files are public government environmental observations.
Follow AirNow's data-use guidelines and preserve source attribution.
Do not represent preliminary values as certified regulatory measurements.
Do not make automated health, employment, insurance, or legal decisions from one observation alone.
Apply appropriate expert review to safety-critical workflows.

### Related official-data automation

Combine this Actor with [Weather.gov Active Alerts Tracker](https://apify.com/automation-lab/us-active-weather-alerts-tracker) for a broader U.S. environmental and operational monitoring workflow.

The weather-alert Actor supplies active NWS warnings.
This Actor supplies dated AirNow site observations and AQI values.
Together they can feed a scheduled situational dashboard while preserving official source links.

### Frequently asked questions

**Does this Actor require an EPA or AirNow key?**

No.
It uses the public official downloadable daily files.

**Does it return forecasts?**

No.
It returns peak and average daily monitoring-site observations from `daily_data_v2.dat`.

**Can it download historical dates?**

Yes, when AirNow retains a dated file at the documented path.
Supply individual dates or an inclusive range of up to 31 days.

**Can I select several pollutants?**

Yes.
The filter accepts base pollutant names and exact parameter labels.

**Are AQI labels calculated by the Actor?**

The numeric AQI and category code come from AirNow.
The Actor maps AirNow's documented category code to its EPA descriptor.

**Can I export more than 1,000 rows?**

Yes.
Increase `maxItems` up to 50,000.
Only matching saved records incur the per-observation event.

**Is this regulatory AQS data?**

No.
It is preliminary AirNow reporting data.
Use EPA AQS for validated regulatory analyses.

# Actor input Schema

## `dates` (type: `array`):

Specific dates in YYYY-MM-DD format (maximum 31). Leave empty to use today's UTC date.

## `startDate` (type: `string`):

First date in an inclusive range, in YYYY-MM-DD format. Use with endDate instead of dates.

## `endDate` (type: `string`):

Last date in an inclusive range of at most 31 days, in YYYY-MM-DD format.

## `places` (type: `array`):

Case-insensitive text matched against site name, reporting agency, and site identifiers, for example Sacramento or California.

## `siteIds` (type: `array`):

Exact AirNow nine-character site IDs or full AQS IDs.

## `pollutants` (type: `array`):

Pollutant names such as PM2.5, OZONE, PM10, CO, NO2, or SO2; full parameters such as OZONE-8HR are also accepted.

## `countryCodes` (type: `array`):

Three-digit country prefixes from the full AQS identifier, for example 840 for the United States or 124 for Canada.

## `stateFips` (type: `array`):

Two-digit state FIPS codes for U.S. monitoring sites, for example 06 for California or 36 for New York.

## `onlyWithAqi` (type: `boolean`):

When enabled, omit valid concentration records for which AirNow does not calculate an 8-hour or 24-hour AQI.

## `maxItems` (type: `integer`):

Maximum number of matching records saved across all requested dates.

## Actor input object example

```json
{
  "dates": [
    "2025-09-01"
  ],
  "pollutants": [
    "PM2.5",
    "OZONE"
  ],
  "countryCodes": [
    "840"
  ],
  "onlyWithAqi": true,
  "maxItems": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset containing normalized monitoring-site pollutant concentrations and AQI values.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "dates": [
        "2025-09-01"
    ],
    "pollutants": [
        "PM2.5",
        "OZONE"
    ],
    "countryCodes": [
        "840"
    ],
    "onlyWithAqi": true,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/epa-air-quality-aqi-lookup").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "dates": ["2025-09-01"],
    "pollutants": [
        "PM2.5",
        "OZONE",
    ],
    "countryCodes": ["840"],
    "onlyWithAqi": True,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/epa-air-quality-aqi-lookup").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "dates": [
    "2025-09-01"
  ],
  "pollutants": [
    "PM2.5",
    "OZONE"
  ],
  "countryCodes": [
    "840"
  ],
  "onlyWithAqi": true,
  "maxItems": 20
}' |
apify call automation-lab/epa-air-quality-aqi-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/epa-air-quality-aqi-lookup"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/cZxjRcBQnTMfF0y3Q/builds/Y8CKhjQXPhi18ZfhY/openapi.json
